Skip to content

Commit f13151f

Browse files
Marsupkanongil
andauthoredOct 23, 2024··
chore: next version (#1079)
* chore: change CI target for next * chore: add next branch to CI targets * feat: target ESLint v9 * Tighten type checking config (#1071) * Use more restrictive type signatures * Don't rely on broken expect.error() * chore: report node 18+ support (#1081) * chore: update documentation for eslint ignore --------- Co-authored-by: Gil Pedersen <[email protected]>
1 parent 4a2b357 commit f13151f

30 files changed

+233
-96
lines changed
 

‎.eslintignore

-9
This file was deleted.

‎.github/workflows/ci-module.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ on:
44
push:
55
branches:
66
- master
7+
- next
78
pull_request:
89
workflow_dispatch:
910

1011
jobs:
1112
test:
12-
uses: hapijs/.github/.github/workflows/ci-module.yml@master
13-
with:
14-
min-node-version: 14
13+
uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21

‎API.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -632,10 +632,17 @@ Your project's eslint configuration will now extend the default **lab** configur
632632

633633
### Ignoring files in linting
634634

635-
Since [eslint](http://eslint.org/) is used to lint, you can create an `.eslintignore` containing paths to be ignored:
636-
```
637-
node_modules/*
638-
**/vendor/*.js
635+
Since [eslint](http://eslint.org/) is used to lint, if you don't already have an `eslint.config.{js|cjs|mjs|ts|mts|cts}` you can create one,
636+
and add an `ignores` rule containing paths to be ignored. Here is an example preserving default hapi rules:
637+
```javascript
638+
import HapiPlugin from '@hapi/eslint-plugin';
639+
640+
export default [
641+
{
642+
ignores: ['node_modules/*', '**/vendor/*.js'],
643+
},
644+
...HapiPlugin.configs.module,
645+
];
639646
```
640647

641648
### Only run linting

‎eslint.config.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
const HapiPlugin = require('@hapi/eslint-plugin');
4+
5+
module.exports = [
6+
{
7+
ignores: [
8+
'node_modules/',
9+
'test_runner/',
10+
'test/coverage/',
11+
'test/cli/',
12+
'test/cli_*/',
13+
'test/lint/',
14+
'test/override/',
15+
'test/plan/',
16+
'test/transform/'
17+
]
18+
},
19+
...HapiPlugin.configs.module
20+
];

‎lib/linter/.eslintrc.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
22

3-
module.exports = {
4-
extends: 'plugin:@hapi/module'
5-
};
3+
const HapiPlugin = require('@hapi/eslint-plugin');
4+
5+
module.exports = [...HapiPlugin.configs.module];

‎lib/linter/index.js

+40-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
const Fs = require('fs');
4-
const Path = require('path');
54

65
const Eslint = require('eslint');
76
const Hoek = require('@hapi/hoek');
@@ -18,31 +17,50 @@ exports.lint = async function () {
1817

1918
const options = process.argv[2] ? JSON.parse(process.argv[2]) : undefined;
2019

21-
if (!Fs.existsSync('.eslintrc.js') &&
22-
!Fs.existsSync('.eslintrc.cjs') && // Needed for projects with "type": "module"
23-
!Fs.existsSync('.eslintrc.yaml') &&
24-
!Fs.existsSync('.eslintrc.yml') &&
25-
!Fs.existsSync('.eslintrc.json') &&
26-
!Fs.existsSync('.eslintrc')) {
27-
configuration.overrideConfigFile = Path.join(__dirname, '.eslintrc.js');
20+
let usingDefault = false;
21+
22+
if (!Fs.existsSync('eslint.config.js') &&
23+
!Fs.existsSync('eslint.config.cjs') &&
24+
!Fs.existsSync('eslint.config.mjs') &&
25+
!Fs.existsSync('eslint.config.ts') &&
26+
!Fs.existsSync('eslint.config.mts') &&
27+
!Fs.existsSync('eslint.config.cts')) {
28+
// No configuration file found, using the default one
29+
usingDefault = true;
30+
configuration.baseConfig = require('./.eslintrc.js');
31+
configuration.overrideConfigFile = true;
2832
}
2933

3034
if (options) {
3135
Hoek.merge(configuration, options, true, false);
3236
}
3337

34-
if (!configuration.extensions) {
35-
configuration.extensions = ['.js', '.cjs', '.mjs'];
38+
// Only the default configuration should be altered, otherwise the user's configuration should be used as is
39+
if (usingDefault) {
40+
if (!configuration.extensions) {
41+
const extensions = ['js', 'cjs', 'mjs'];
3642

37-
if (configuration.typescript) {
38-
configuration.extensions.push('.ts');
43+
if (configuration.typescript) {
44+
extensions.push('ts');
45+
}
46+
47+
configuration.baseConfig.unshift({
48+
files: extensions.map((ext) => `**/*.${ext}`)
49+
});
3950
}
40-
}
4151

42-
if (configuration.typescript) {
43-
delete configuration.typescript;
52+
if (configuration.ignores) {
53+
configuration.baseConfig.unshift({
54+
ignores: configuration.ignores
55+
});
56+
}
4457
}
4558

59+
delete configuration.extensions;
60+
delete configuration.typescript;
61+
delete configuration.ignores;
62+
63+
4664
let results;
4765
try {
4866
const eslint = new Eslint.ESLint(configuration);
@@ -66,6 +84,13 @@ exports.lint = async function () {
6684

6785
transformed.errors = result.messages.map((err) => {
6886

87+
if (err.messageTemplate === 'all-matched-files-ignored') {
88+
return {
89+
severity: 'ERROR',
90+
message: err.message
91+
};
92+
}
93+
6994
return {
7095
line: err.line,
7196
severity: err.severity === 1 ? 'WARNING' : 'ERROR',

‎lib/modules/coverage.js

+34-6
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ const SourceMap = require('../source-map');
1616
const Transform = require('./transform');
1717

1818
const internals = {
19-
_state: Symbol.for('@hapi/lab/coverage/_state'),
20-
eslint: new ESLint.ESLint({ baseConfig: Eslintrc })
19+
_state: Symbol.for('@hapi/lab/coverage/_state')
2120
};
2221

2322

@@ -111,7 +110,7 @@ internals.prime = function (extension, ctx) {
111110
require.extensions[extension] = function (localModule, filename) {
112111

113112
// We never want to instrument eslint configs in order to avoid infinite recursion
114-
if (Path.basename(filename, extension) !== '.eslintrc') {
113+
if (!['.eslintrc', 'eslint.config'].includes(Path.basename(filename, extension))) {
115114
for (let i = 0; i < internals.state.patterns.length; ++i) {
116115
if (internals.state.patterns[i].test(filename.replace(/\\/g, '/'))) {
117116
return localModule._compile(internals.instrument(filename, ctx), filename);
@@ -761,11 +760,40 @@ internals.file = async function (filename, data, options) {
761760

762761
internals.context = async (options) => {
763762

763+
const filePath = Path.join(options.coveragePath || '', 'x.js');
764+
let calculated;
765+
764766
// The parserOptions are shared by all files for coverage purposes, based on
765767
// the effective eslint config for a hypothetical file {coveragePath}/x.js
766-
const { parserOptions } = await internals.eslint.calculateConfigForFile(
767-
Path.join(options.coveragePath || '', 'x.js')
768-
);
768+
try {
769+
// Let's try first with eslint's native configuration detection
770+
const eslint = new ESLint.ESLint({
771+
ignore: false
772+
});
773+
774+
calculated = await eslint.calculateConfigForFile(filePath);
775+
}
776+
catch (err) {
777+
/* $lab:coverage:off$ */
778+
if (err.messageTemplate !== 'config-file-missing') {
779+
throw err;
780+
}
781+
782+
// If the eslint config file is missing, we'll use the one provided by lab
783+
const eslint = new ESLint.ESLint({
784+
overrideConfig: Eslintrc,
785+
overrideConfigFile: true,
786+
ignore: false
787+
});
788+
789+
calculated = await eslint.calculateConfigForFile(filePath);
790+
/* $lab:coverage:on$ */
791+
}
792+
793+
const parserOptions = {
794+
...calculated.languageOptions,
795+
...calculated.languageOptions?.parserOptions
796+
};
769797

770798
return { parserOptions };
771799
};

‎lib/modules/lint.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ exports.lint = function (settings) {
2020
try {
2121
linterOptions = JSON.parse(settings['lint-options'] || '{}');
2222
}
23-
catch (err) {
23+
catch {
2424
return reject(new Error('lint-options could not be parsed'));
2525
}
2626

‎lib/modules/transform.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ exports.retrieveFile = function (path) {
7373
try {
7474
contents = Fs.readFileSync(path, 'utf8');
7575
}
76-
catch (e) {
76+
catch {
7777
contents = null;
7878
}
7979

‎lib/modules/types.js

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const Utils = require('../utils');
1515
const internals = {
1616
compiler: {
1717
strict: true,
18+
noUncheckedIndexedAccess: true,
19+
exactOptionalPropertyTypes: true,
1820
jsx: Ts.JsxEmit.React,
1921
lib: ['lib.es2020.d.ts'],
2022
module: Ts.ModuleKind.CommonJS,

‎lib/modules/typescript.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internals.transform = function (content, fileName) {
1414
try {
1515
var { config, error } = Typescript.readConfigFile(configFile, Typescript.sys.readFile);
1616
}
17-
catch (err) {
17+
catch {
1818
throw new Error(`Cannot find a tsconfig file for ${fileName}`);
1919
}
2020

‎lib/runner.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ const internals = {};
1313

1414
// Prevent libraries like Sinon from clobbering global time functions
1515

16+
/* eslint-disable no-redeclare */
1617
const Date = global.Date;
1718
const setTimeout = global.setTimeout;
1819
const clearTimeout = global.clearTimeout;
1920
const setImmediate = global.setImmediate;
21+
/* eslint-enable no-redeclare */
2022

2123

2224
Error.stackTraceLimit = Infinity; // Set Error stack size

‎package.json

+11-12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"repository": "git://github.com/hapijs/lab",
66
"main": "lib/index.js",
77
"types": "lib/index.d.ts",
8+
"engines": {
9+
"node": ">=18"
10+
},
811
"keywords": [
912
"test",
1013
"runner"
@@ -13,19 +16,14 @@
1316
"bin/lab",
1417
"lib"
1518
],
16-
"eslintConfig": {
17-
"extends": [
18-
"plugin:@hapi/module"
19-
]
20-
},
2119
"dependencies": {
2220
"@babel/core": "^7.16.0",
23-
"@babel/eslint-parser": "^7.16.0",
21+
"@babel/eslint-parser": "^7.25.1",
2422
"@hapi/bossy": "^6.0.0",
25-
"@hapi/eslint-plugin": "^6.0.0",
23+
"@hapi/eslint-plugin": "^7.0.0",
2624
"@hapi/hoek": "^11.0.2",
2725
"diff": "^5.0.0",
28-
"eslint": "8.x.x",
26+
"eslint": "9.x.x",
2927
"find-rc": "4.x.x",
3028
"globby": "^11.1.0",
3129
"handlebars": "4.x.x",
@@ -37,8 +35,8 @@
3735
"will-call": "1.x.x"
3836
},
3937
"peerDependencies": {
40-
"@hapi/eslint-plugin": "^6.0.0",
41-
"typescript": ">=3.6.5"
38+
"@hapi/eslint-plugin": "^7.0.0",
39+
"typescript": ">=4.4.0"
4240
},
4341
"peerDependenciesMeta": {
4442
"typescript": {
@@ -48,13 +46,14 @@
4846
"devDependencies": {
4947
"@hapi/code": "^9.0.0",
5048
"@hapi/somever": "^4.0.0",
49+
"@types/eslint": "^9.6.0",
5150
"@types/node": "^18.11.17",
52-
"@typescript-eslint/parser": "^5.62.0",
5351
"cpr": "3.x.x",
5452
"lab-event-reporter": "1.x.x",
5553
"semver": "7.x.x",
5654
"tsconfig-paths": "^4.0.0",
57-
"typescript": "^4.5.4"
55+
"typescript": "^4.5.4",
56+
"typescript-eslint": "^8.1.0"
5857
},
5958
"bin": {
6059
"lab": "./bin/lab"

‎test/cli.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Load modules
44

55
const ChildProcess = require('child_process');
6+
// eslint-disable-next-line no-redeclare
67
const Crypto = require('crypto');
78
const Fs = require('fs');
89
const Http = require('http');
@@ -702,7 +703,7 @@ describe('CLI', () => {
702703
try {
703704
await unlink(outputPath);
704705
}
705-
catch (err) {
706+
catch {
706707

707708
// Error is ok here
708709
}

‎test/coverage.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -566,19 +566,19 @@ describe('Coverage', () => {
566566
it('sorts file paths in report', async () => {
567567

568568
const files = global.__$$labCov.files;
569-
const paths = ['/a/b', '/a/b/c', '/a/c/b', '/a/c', '/a/b/c', '/a/b/a'];
569+
const paths = ['./a/b', './a/b/c', './a/c/b', './a/c', './a/b/c', './a/b/a'];
570570
paths.forEach((path) => {
571571

572572
files[path] = { source: [] };
573573
});
574574

575-
const cov = await Lab.coverage.analyze({ coveragePath: '/a' });
575+
const cov = await Lab.coverage.analyze({ coveragePath: './a' });
576576
const sorted = cov.files.map((file) => {
577577

578578
return file.filename;
579579
});
580580

581-
expect(sorted).to.equal(['/a/b', '/a/c', '/a/b/a', '/a/b/c', '/a/c/b']);
581+
expect(sorted).to.equal(['./a/b', './a/c', './a/b/a', './a/b/c', './a/c/b']);
582582
});
583583
});
584584

Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
'use strict';
2+
3+
const HapiPlugin = require('@hapi/eslint-plugin');
4+
15
// this is a deliberately unused function that will reduce coverage percentage
26
// if it ends up getting instrumented, giving us something to assert against
37
const unusedMethod = () => {
48
console.log('hello world')
59
}
610

7-
module.exports = {
8-
extends: 'plugin:@hapi/module'
9-
}
11+
module.exports = [...HapiPlugin.configs.module]
+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
// Load modules
4-
4+
const EslintConfig = require('./eslint.config');
55

66
// Declare internals
77

@@ -10,5 +10,5 @@ const internals = {};
1010

1111
exports.method = function () {
1212

13-
return;
13+
return EslintConfig;
1414
};

‎test/lint/eslint/esm/.eslintrc.cjs

-13
This file was deleted.
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
const HapiPlugin = require('@hapi/eslint-plugin');
4+
5+
module.exports = [
6+
...HapiPlugin.configs.module,
7+
{
8+
languageOptions: {
9+
parserOptions: {
10+
sourceType: 'module'
11+
}
12+
}
13+
},
14+
{
15+
files: ['*.cjs'],
16+
languageOptions: {
17+
parserOptions: {
18+
sourceType: 'script'
19+
}
20+
}
21+
}
22+
];

‎test/lint/eslint/typescript/.eslintrc.cjs

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
const HapiPlugin = require('@hapi/eslint-plugin');
4+
const TsESLint = require('typescript-eslint');
5+
6+
module.exports = TsESLint.config(
7+
{
8+
files: ['**/*.ts']
9+
},
10+
...HapiPlugin.configs.module,
11+
TsESLint.configs.base
12+
);

‎test/lint/eslint/with_config/.eslintignore

-1
This file was deleted.

‎test/lint/eslint/with_config/.eslintrc.js

-9
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = [
4+
{
5+
ignores: ['*.ignore.*']
6+
},
7+
{
8+
'rules': {
9+
'eol-last': 2,
10+
'no-unused-vars': 0,
11+
'no-undef': 0
12+
}
13+
}
14+
];
15+

‎test/linters.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('Linters - eslint', () => {
8888
{ line: 12, severity: 'WARNING', message: 'eol-last - Newline required at end of file but not found.' }
8989
]);
9090

91-
const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, '.eslintrc.cjs'));
91+
const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, 'eslint.config.cjs'));
9292
expect(checkedCjsFile.errors).to.be.empty();
9393
});
9494

@@ -131,7 +131,6 @@ describe('Linters - eslint', () => {
131131
const checkedFile = eslintResults.find(({ filename }) => filename.endsWith('.ts'));
132132
expect(checkedFile).to.include({ filename: Path.join(path, 'fail.ts') });
133133
expect(checkedFile.errors).to.include([
134-
{ line: 1, severity: 'ERROR', message: `strict - Use the global form of 'use strict'.` },
135134
{ line: 6, severity: 'ERROR', message: 'indent - Expected indentation of 4 spaces but found 1 tab.' },
136135
{ line: 6, severity: 'ERROR', message: 'semi - Missing semicolon.' }
137136
]);
@@ -195,15 +194,15 @@ describe('Linters - eslint', () => {
195194

196195
it('should pass options and not find any files', async () => {
197196

198-
const lintOptions = JSON.stringify({ extensions: ['.jsx'] });
197+
const lintOptions = JSON.stringify({ extensions: ['.jsx'], ignores: ['**/*.js'] });
199198
const path = Path.join(__dirname, 'lint', 'eslint', 'basic');
200199
const result = await Linters.lint({ lintingPath: path, linter: 'eslint', 'lint-options': lintOptions });
201200

202201
expect(result).to.include('lint');
203202

204203
const eslintResults = result.lint;
205204
expect(eslintResults).to.have.length(1);
206-
expect(eslintResults[0].errors[0].message).to.contain('No files');
205+
expect(eslintResults[0].errors[0].message).to.contain('All files matched by \'.\' are ignored.');
207206
});
208207

209208
it('should fix lint rules when --lint-fix used', async (flags) => {

‎test/reporters.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
// eslint-disable-next-line no-redeclare
34
const Crypto = require('crypto');
45
const Fs = require('fs/promises');
56
const Os = require('os');

‎test/runner.js

+2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ const expect = Code.expect;
2525

2626
// save references to timer globals
2727

28+
/* eslint-disable no-redeclare */
2829
const setTimeout = global.setTimeout;
2930
const clearTimeout = global.clearTimeout;
3031
const setImmediate = global.setImmediate;
32+
/* eslint-enable no-redeclare */
3133

3234
describe('Runner', () => {
3335

‎test/types.js

+12
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ describe('Types', () => {
9898
line: 3,
9999
column: 4
100100
},
101+
{
102+
filename: 'test/restrict.ts',
103+
message: `Type 'undefined' is not assignable to type 'string' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.`,
104+
line: 10,
105+
column: 0
106+
},
107+
{
108+
filename: 'test/restrict.ts',
109+
message: `'unchecked.b' is possibly 'undefined'.`,
110+
line: 15,
111+
column: 0
112+
},
101113
{
102114
filename: 'test/syntax.ts',
103115
message: `')' expected.`,

‎test/types/errors/lib/index.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,14 @@ export default add;
88
export const sample: { readonly x: string };
99

1010
export function hasProperty(property: { name: string }): boolean;
11+
12+
export interface UsesExactOptionalPropertyTypes {
13+
a?: boolean | undefined;
14+
b?: string;
15+
}
16+
17+
export interface UncheckedIndexedAccess {
18+
a: UsesExactOptionalPropertyTypes;
19+
20+
[prop: string]: UsesExactOptionalPropertyTypes;
21+
}

‎test/types/errors/test/restrict.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Lab from '../../../..';
2+
import type { UncheckedIndexedAccess, UsesExactOptionalPropertyTypes } from '../lib/index';
3+
4+
const { expect } = Lab.types;
5+
6+
const exact: UsesExactOptionalPropertyTypes = { a: true };
7+
8+
exact.a = undefined;
9+
exact.b = 'ok';
10+
exact.b = undefined; // Fails
11+
12+
const unchecked: UncheckedIndexedAccess = { a: exact, b: {} };
13+
14+
unchecked.a.a;
15+
unchecked.b.a; // Fails

0 commit comments

Comments
 (0)
Please sign in to comment.