Switch to ESLint v9 and flat config (#3558)

Since PR #3551 was not yet complete, I made my own attempt.

1. Update to ESLint v9.
2. Replace deprecated `.eslintrc.json` and `.eslintignore` by flat
config `eslint.config.mjs`.
3. Adapt `check_config.js` to use flat config.
4. Since `eslint-plugin-import` still doesn't support ESLint v9 I
removed it. We can add it back when it does support v9.
5. Run tests `npm run check:js` and `npm run config:check`.
6. In order not to overload this PR, I have not yet activated more
additional rules - there are some useful ones in the new plugin
`@eslint/js`.

@bugsounet, please don't take it as an offence that I have created a
competing PR. The migration to ESLint v9 has been burning under my nails
for some time.
This commit is contained in:
Kristjan ESPERANTO 2024-09-25 21:05:11 +02:00 committed by GitHub
parent 5ffdf9af09
commit d3187689f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 372 additions and 1154 deletions

View File

@ -1,3 +0,0 @@
modules/*
!modules/default/
js/positions.js

View File

@ -1,94 +0,0 @@
{
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends", "plugin:import/recommended", "plugin:jest/recommended", "plugin:jsdoc/recommended"],
"plugins": ["unicorn"],
"env": {
"browser": true,
"es2024": true,
"jest/globals": true,
"node": true
},
"globals": {
"config": true,
"Log": true,
"MM": true,
"Module": true,
"moment": true
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest",
"ecmaFeatures": {
"globalReturn": true
}
},
"rules": {
"eqeqeq": "error",
"import/order": "error",
"import/extensions": "error",
"import/newline-after-import": "error",
"jest/consistent-test-it": "warn",
"jest/expect-expect": "warn",
"jest/no-done-callback": "warn",
"jest/prefer-expect-resolves": "warn",
"jest/prefer-mock-promise-shorthand": "warn",
"jest/prefer-to-be": "warn",
"jest/prefer-to-have-length": "warn",
"no-param-reassign": "error",
"no-prototype-builtins": "off",
"no-throw-literal": "error",
"no-unused-vars": "off",
"no-useless-return": "error",
"object-shorthand": ["error", "methods"],
"prefer-template": "error",
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": "off",
"@stylistic/comma-dangle": ["error", "never"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-paren-newline": ["error", "consistent"],
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
"@stylistic/max-statements-per-line": ["error", { "max": 2 }],
"@stylistic/multiline-comment-style": "off",
"@stylistic/multiline-ternary": ["error", "always-multiline"],
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }],
"@stylistic/no-extra-parens": "off",
"@stylistic/no-tabs": "off",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }],
"@stylistic/operator-linebreak": ["error", "before"],
"@stylistic/padded-blocks": "off",
"@stylistic/quote-props": ["error", "as-needed"],
"@stylistic/quotes": ["error", "double"],
"@stylistic/indent": ["error", "tab"],
"@stylistic/semi": ["error", "always"],
"@stylistic/space-before-function-paren": ["error", "always"],
"@stylistic/spaced-comment": "off",
"unicorn/prefer-node-protocol": "error"
},
"overrides": [
{
"files": ["config/config.js*"],
"rules": {
"@stylistic/comma-dangle": "off",
"@stylistic/indent": "off",
"@stylistic/no-multi-spaces": "off"
}
},
{
"files": ["tests/configs/modules/weather/*.js"],
"rules": {
"@stylistic/quotes": "off"
}
},
{
"extends": ["plugin:package-json/recommended"],
"files": ["package.json"],
"parser": "jsonc-eslint-parser",
"plugins": ["package-json"],
"rules": {
"package-json/sort-collections": ["error", ["devDependencies", "dependencies", "peerDependencies", "config"]]
}
}
]
}

View File

@ -12,7 +12,7 @@ We use prettier for automatic linting of all our files: `npm run lint:prettier`.
We use [ESLint](https://eslint.org) on our JavaScript files.
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
The ESLint configuration is in our `eslint.config.mjs` file.
To run ESLint, use `npm run lint:js`.

View File

@ -1,5 +1,5 @@
*.js
.eslintignore
*.mjs
.husky/pre-commit
.prettierignore
/config

View File

@ -23,6 +23,7 @@ _This release is scheduled to be released on 2024-10-01._
- [core] removed installer only files (#3492)
- [core] removed raspberry object from systeminformation (#3505)
- [linter] removed `eslint-plugin-import`, because it doesn't support ESLint v9. We will reenter it later when it does.
### Updated
@ -32,6 +33,7 @@ _This release is scheduled to be released on 2024-10-01._
- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded (PR #3518 fixes issue #3504)
- [core] Detail optimizations in `config_check.js`
- [core] Updated minimal needed node version in `package.json` (currently v20.9.0)
- [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
### Fixed

121
eslint.config.mjs Normal file
View File

@ -0,0 +1,121 @@
import eslintPluginJest from "eslint-plugin-jest";
import eslintPluginJs from "@eslint/js";
import eslintPluginStylistic from "@stylistic/eslint-plugin";
import globals from "globals";
const config = [
eslintPluginJs.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
...globals.jest,
Log: "readonly",
MM: "readonly",
Module: "readonly",
config: "readonly",
moment: "readonly"
}
},
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins,
...eslintPluginJest.configs["flat/recommended"].plugins
},
rules: {
...eslintPluginStylistic.configs["all-flat"].rules,
...eslintPluginJest.configs["flat/recommended"].rules,
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": "off",
"@stylistic/comma-dangle": ["error", "never"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-paren-newline": ["error", "consistent"],
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
"@stylistic/indent": ["error", "tab"],
"@stylistic/max-statements-per-line": ["error", {max: 2}],
"@stylistic/multiline-comment-style": "off",
"@stylistic/multiline-ternary": ["error", "always-multiline"],
"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}],
"@stylistic/no-extra-parens": "off",
"@stylistic/no-tabs": "off",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
"@stylistic/operator-linebreak": ["error", "before"],
"@stylistic/padded-blocks": "off",
"@stylistic/quote-props": ["error", "as-needed"],
"@stylistic/quotes": ["error", "double"],
"@stylistic/semi": ["error", "always"],
"@stylistic/space-before-function-paren": ["error", "always"],
"@stylistic/spaced-comment": "off",
eqeqeq: "error",
"id-length": "off",
"init-declarations": "off",
"jest/consistent-test-it": "warn",
"jest/no-done-callback": "warn",
"jest/prefer-expect-resolves": "warn",
"jest/prefer-mock-promise-shorthand": "warn",
"jest/prefer-to-be": "warn",
"jest/prefer-to-have-length": "warn",
"max-lines-per-function": ["warn", 350],
"max-statements": "off",
"no-global-assign": "off",
"no-inline-comments": "off",
"no-magic-numbers": "off",
"no-param-reassign": "error",
"no-plusplus": "off",
"no-prototype-builtins": "off",
"no-ternary": "off",
"no-throw-literal": "error",
"no-undefined": "off",
"no-unused-vars": "off",
"no-useless-return": "error",
"no-warning-comments": "off",
"object-shorthand": ["error", "methods"],
"one-var": "off",
"prefer-destructuring": "off",
"prefer-template": "error",
"sort-keys": "off"
}
},
{
files: ["**/*.mjs"],
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.node
},
sourceType: "module"
},
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins
},
rules: {
...eslintPluginStylistic.configs["all-flat"].rules,
"@stylistic/array-element-newline": "off",
"@stylistic/indent": ["error", "tab"],
"@stylistic/padded-blocks": ["error", "never"],
"@stylistic/quote-props": ["error", "as-needed"],
"func-style": "off",
"import/namespace": "off",
"max-lines-per-function": ["error", 100],
"no-magic-numbers": "off",
"one-var": "off",
"prefer-destructuring": "off"
}
},
{
files: ["tests/configs/modules/weather/*.js"],
rules: {
"@stylistic/quotes": "off"
}
},
{
ignores: ["config/**", "modules/**", "!modules/default/**", "js/positions.js"]
}
];
export default config;

View File

@ -2,13 +2,14 @@ const path = require("node:path");
const fs = require("node:fs");
const Ajv = require("ajv");
const colors = require("ansis");
const globals = require("globals");
const { Linter } = require("eslint");
const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`);
const Utils = require(`${rootPath}/js/utils.js`);
const linter = new Linter();
const linter = new Linter({ configType: "flat" });
const ajv = new Ajv();
/**
@ -29,16 +30,14 @@ function checkConfigFile () {
// Check if file is present
if (fs.existsSync(configFileName) === false) {
Log.error(`File not found: ${configFileName}`);
throw new Error("No config file present!");
throw new Error(`File not found: ${configFileName}\nNo config file present!`);
}
// Check permission
try {
fs.accessSync(configFileName, fs.F_OK);
} catch (error) {
Log.error(error);
throw new Error("No permission to access config file!");
throw new Error(`${error}\nNo permission to access config file!`);
}
// Validate syntax of the configuration file.
@ -47,30 +46,39 @@ function checkConfigFile () {
// I'm not sure if all ever is utf-8
const configFile = fs.readFileSync(configFileName, "utf-8");
// Explicitly tell linter that he might encounter es2024 syntax ("let config = {...}")
const errors = linter.verify(configFile, {
env: {
es2024: true
}
});
const errors = linter.verify(
configFile,
{
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.node
}
}
},
configFileName
);
if (errors.length === 0) {
Log.info(colors.green("Your configuration file doesn't contain syntax errors :)"));
validateModulePositions(configFileName);
} else {
Log.error("Your configuration file contains syntax errors :(");
let errorMessage = "Your configuration file contains syntax errors :(";
for (const error of errors) {
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
}
process.exit(1);
throw new Error(errorMessage);
}
}
function validateModulePositions (configFileName) {
Log.info("Checking modules structure configuration ...");
const positionList = Utils.getModulePositions();
// Make Ajv schema configuration of modules config
// only scan "module" and "position"
// Only scan "module" and "position"
const schema = {
type: "object",
properties: {
@ -103,16 +111,21 @@ function checkConfigFile () {
} else {
const module = validate.errors[0].instancePath.split("/")[2];
const position = validate.errors[0].instancePath.split("/")[3];
Log.error("This module configuration contains errors:");
Log.error(`\n${JSON.stringify(data.modules[module], null, 2)}`);
let errorMessage = "This module configuration contains errors:";
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
if (position) {
Log.error(`${position}: ${validate.errors[0].message}`);
Log.error(`\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`);
errorMessage += `\n${position}: ${validate.errors[0].message}`;
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
} else {
Log.error(validate.errors[0].message);
errorMessage += validate.errors[0].message;
}
Log.error(errorMessage);
}
}
checkConfigFile();
try {
checkConfigFile();
} catch (error) {
Log.error(error.message);
process.exit(1);
}

1238
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@
"ansis": "^3.3.2",
"console-stamp": "^3.1.2",
"envsub": "^4.1.0",
"eslint": "^8.57.0",
"eslint": "^9.11.1",
"express": "^4.21.0",
"express-ipfilter": "^1.3.2",
"feedme": "^2.0.2",
@ -75,13 +75,12 @@
"systeminformation": "^5.23.5"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@stylistic/eslint-plugin": "^2.8.0",
"cspell": "^8.14.4",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-jsdoc": "^50.2.4",
"eslint-plugin-package-json": "^0.15.3",
"eslint-plugin-unicorn": "^55.0.0",
"express-basic-auth": "^1.2.1",
"husky": "^9.1.6",
"jest": "^29.7.0",

View File

@ -19,7 +19,7 @@ describe("server_functions tests", () => {
},
text: fetchResponseHeadersText
};
// eslint-disable-next-line
fetch = jest.fn();
fetch.mockImplementation(() => fetchResponse);