eslint – tsmx https://tsmx.net pragmatic IT Tue, 20 Aug 2024 19:15:30 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 https://tsmx.net/wp-content/uploads/2020/09/cropped-tsmx-klein_transparent-2-32x32.png eslint – tsmx https://tsmx.net 32 32 Migrating eslintrc.json to eslint.config.js in a CommonJS project https://tsmx.net/migrating-eslintrc-to-flat-config-in-commonjs/ Mon, 19 Aug 2024 20:37:10 +0000 https://tsmx.net/?p=3036 Read more]]> A practical end-to-end guide for migrating an existing .eslintrc.json config (ESLint v8 and before) to the new flat file config eslint.config.js (ESLint v9 and above) in a CommonJS Node.js project including linting of unit-tests with Jest.

This article comes along with a public GitHub example repository enabling you to comprehend everything and easily switch between before/after migration state.

Starting point: existing eslintrc configuration

In this migration guide we’ll use a very standard ESLint configuration set which should cover basic linting for the vast majority of your projects.

  • Configure ESLint for use with Node.js and CommonJS using a specified ECMA version
  • Ensure a proper linting of Jest tests
  • Use a predefined set of linting rules as a starting point
  • Ensure right linting of basics like indention, unused variables, use of globals, semicolons and quote signs used

So far, the usual way to configure ESLint in Node.js was to place an .eslintrc.json file in the root folder of the project. The below .eslintrc.json covers all the mentioned points and serves as the basis for this guide.

{
    "env": {
        "node": true,
        "commonjs": true,
        "es6": true,
        "jest": true
    },
    "extends": "eslint:recommended",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018
    },
    "rules": {
        "indent": [
            "error",
            4,
            {
                "SwitchCase": 1
            }
        ],
        "quotes": [
            "error",
            "single"
        ],
        "semi": [
            "error",
            "always"
        ],
        "no-unused-vars": [
            2,
            {
                "args": "after-used",
                "argsIgnorePattern": "^_"
            }
        ]
    }
}

Additionally, you might have a .eslintignore file placed in the root folder of the project to exclude files and paths from linting, e.g. to exclude the two directories conf and coverage – like so:

conf/
coverage/

Errors after upgrading ESLint to v9

Having this configuration in place you’ll notice that your environment, in this case VSCode, is coming up with an error after upgrading to ESLint v9. The highlighting of linting errors and warnings also isn’t working any more.

eslint-config-error-vscode

Having a look in the ESLint output quickly gives you the reason why.

[Info  - 20:51:44] ESLint server is starting.
[Info  - 20:51:44] ESLint server running in node v20.14.0
[Info  - 20:51:44] ESLint server is running.
[Info  - 20:51:46] ESLint library loaded from: /home/tsmx/projects/weather-tools/node_modules/eslint/lib/api.js
(node:4117) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files
(Use `code --trace-warnings ...` to show where the warning was created)
[Error - 20:51:46] Calculating config file for file:///home/tsmx/projects/weather-tools/weather-tools.js) failed.
Error: Could not find config file.

Starting with ESLint v9.0.0, the default configuration was changed to flat file and eslintrc.json as well as eslintignore became deprecated. Although it’s possible to continue using eslintrc.json, it’s recommended to switch to the new file format being future-proof.

Migrating to the new flat file configuration

For a CommonJS project, the new flat file configuration is a normal JavaScript file called eslint.config.js which is placed in the root folder and simply exports an array of ESLint configuration objects via module.exports.

Installing needed dev dependencies

The flat file config doesn’t contain an env section anymore that allows you to specify ESLint is running in Node.js and enabling Jest features for correct linting of unit-test files. Also, the recommended ruleset has been outsourced to an own module.

To include all these features in the new ESLint v9 configuration, you’ll need to install the following dependencies in your project.

  • @eslint/js for using the recommended ruleset as a basis
  • eslint-plugin-jest to enable proper linting of Jest test files
  • globals to make ESLint aware of common global variables for Node.js and Jest avoiding they are marked as undefined

As these dependencies are only used for ESLint, you should install them – like ESLint itself – as dev dependencies in your Node.js project.

# npm install @eslint/js eslint-plugin-jest globals --save-dev

Creating eslint.config.js

Next, in the root folder of your Node.js project, create an eslint.config.js file with the following contents. This will lead to an almost identical, yet more customizable, linting behaviour as the old .eslintrc.json did.

const { configs } = require('@eslint/js');
const jest = require('eslint-plugin-jest');
const globals = require('globals');

module.exports = [
    configs.recommended,
    {
        languageOptions: {
            ecmaVersion: 2018,
            sourceType: 'commonjs',
            globals: { 
                ...globals.node, 
                ...globals.jest, 
                Atomics: 'readonly', 
                SharedArrayBuffer: 'readonly' 
            }
        },
        rules: {
            semi: 'error',
            quotes: ['error', 'single'],
            indent: ['error', 4, { 'SwitchCase': 1 }],
            'no-unused-vars':
                [
                    'warn',
                    {
                        'varsIgnorePattern': '^_',
                        'args': 'after-used',
                        'argsIgnorePattern': '^_'
                    }
                ]
        },
        ignores: ['conf/', 'coverage/']
    },
    {
        languageOptions: {
            globals: { ...globals.jest }
        },
        files: ['test/*.test.js'],
        ...jest.configs['flat/recommended'],
        rules: {
            ...jest.configs['flat/recommended'].rules
        }
    }
];

Thats’s already it. Linting now should work again as expected and you can safely delete the old .eslintrc.json as well as .eslintignore in your project.

Breakdown of the new flat file configuration

As noted before, the flat file configuration is simply an exported array of ESLint configuration objects. Based on our eslintrc.json we want to migrate, this array will have three entries.

Part 1: Importing recommended ESLint ruleset

First element of the configuration array should be the recommended ruleset that is delivered by the @eslint/js package. This line is the replacement for the "extends": "eslint:recommended" entry in the old eslintrc.

configs.recommended

Part 2: Custom rules for normal JavaScript code files and files to be ignored

Next object in the configuration array holds all our own custom rules and properties for normal JavaScript code files as well as the patterns of all files/folders that shout be ignored bei ESLint.

{
    languageOptions: {
        ecmaVersion: 2018,
        sourceType: 'commonjs',
        globals: { 
            ...globals.node, 
            ...globals.jest, 
            Atomics: 'readonly', 
            SharedArrayBuffer: 'readonly' 
         }
    },
    rules: {
        semi: 'error',
        quotes: ['error', 'single'],
        indent: ['error', 4, { 'SwitchCase': 1 }],
        'no-unused-vars':
            [
                'warn',
                {
                    'varsIgnorePattern': '^_',
                    'args': 'after-used',
                    'argsIgnorePattern': '^_'
                }
            ]
    },
    ignores: ['conf/', 'coverage/']
}

This section is quite self-explanatory when compared to the old eslintrc configuration. The key differences are:

  • There is no env section anymore, most of that configuration is now located under languageOptions.
  • Note that in the globals object all node and jest globals where added explicitly by using the corresponding arrays provided in the globals-package. This ensures that all common Node.js globals like process and Jest globals like expect are not treated as undefined variables. The latter makes sense if you create some kind of test-utils files which use Jest commands but are not unit-test files themselves. See the example GitHub repository for such an example (/tests/test-utils.js).
  • There is now an ignore property that takes an array of files/folders to be ignored by ESLint. The syntax of the entries is the same as it was in .eslintignore which is now obsolete. For more details see ignoring files.

The linting rules themselves are quite unchanged in the new configuration style.

Part 3: Rules for linting Jest test files

The last needed configuration object is for correct linting of Jest tests. There is no "jest": true option anymore which was very simple. Instead, we’ll need to import the eslint-plugin-jest package and use recommended rules out of it. In the example, all Jest test files are located in the projects folder test/ and have the common extension .test.js.

The resulting configuration object for our eslint.config.js is:

{
    languageOptions: {
        globals: { ...globals.jest }
    },
    files: ['test/*.test.js'],
    ...jest.configs['flat/recommended'],
    rules: {
        ...jest.configs['flat/recommended'].rules
    }
}

This ensures a proper linting of all Jest tests located under test/. If you have test files located in other/additional locations, simply add them to the files property.

Note: If you use Node.js globals like process in your Jest tests, you should add ...globals.node to the globals property. This prevents ESLint from reporting those globals as undefined variables.

Example project on GitHub

To see a practical working example of a migration before and after, clone the eslintrc-to-flatfile GitHub repository and browse through the branches eslint-v8 and eslint-v9. The code files contain several example errors in formatting, quoting etc. that should be highlighted as linting errors or warnings. For details see the code comments.

# clone the example project
git clone https://github.com/tsmx/eslintrc-to-flatfile.git

# check out/switch to the original ESLint v8 branch using eslintrc.json
git checkout eslint-v8
npm install

# check out/switch to the migrated ESLint v9 branch using new eslint.config.js
git checkout eslint-v9
npm install

Useful links

]]>
Convert an existing NodeJS project from CommonJS to ESM / ECMAScript https://tsmx.net/convert-existing-nodejs-project-from-commonjs-to-esm/ Thu, 25 May 2023 20:57:25 +0000 https://tsmx.net/?p=1967 Read more]]> A quick guide for converting your existing NodeJS projects from CommonJS to ESM/ECMAScript. Including Express, Jest, Supertest and ESLint.

ECMAScript or ESM modules are the official way of developing JavaScript software today and many projects and libraries are moving towards this format to leverage its advantages. Support for CommonJS, which was the de facto standard for NodeJS projects so far, is even being dropped by some famous libs. The question now is how your existing NodeJS projects can be migrated to the new level?

Fortunately, for the vast majority of projects this should be a no-brainer. In this guide we’ll migrate a minimal project using popular libs and tools like Express, Jest, Supertest and ESLint from CommonJS to an ESM project.

tl;dr – fast-lane conversion

For the impatient here’s the straight forward uncommented list of steps to convert an existing project using Jest and ESLint from CommonJS to ESM. For more details on each step and other sidenotes see further below.

  1. In package.json: add "type": "module" and "exports": "./start.js", remove the "main": "start.js" entry.
  2. Replace all module.exports statements with export and all require statements with import in your source files.
  3. Set or add "sourceType": "module" and "ecmaVersion": "latest" in the parserOptions section of your ESLint configuration.
  4. Replace the "jest" start command in package.json with "NODE_OPTIONS=--experimental-vm-modules npx jest".

That’s already it. You should now be able to run the project and all tests using ESM. Keep on reading for an example and more details on the conversion steps.

Example project on GitHub

This post is accompanied by the node-commonjs-to-esm example project on GitHub which uses Express, Jest, Supertest and ESLint. The project comes with two branches commonjs and esm showing the original state using CommonJS and the migrated one using ESM.

# clone the example project
git clone https://github.com/tsmx/node-commonjs-to-esm.git

# install needed dependencies
cd node-commonjs-to-esm
npm install

# check out the original CommonJS project
git checkout commonjs

# check out the migrated ESM project
git checkout esm

To start the project or test suite – regardless of the branch you are in – run the following commands.

# run the project to start a simple server on localhost:3000 
# with GET routes for '/', '/route1' and '/route2'
npm run start

# run the Jest tests
npm run test

Note: When switching between the ESM and the CommonJS branch, a new npm install is not necessary as the conversion doesn’t affect the dependencies.

Required NodeJS version

To make full use of ECMAScript/ESM and convert your projects accordingly, a NodeJS version of at least v12.20.0 or v14.13.0 is needed.

Please note that if you are on an older version of NodeJS it’s highly recommended to update anyways since these versions are quite old and even support for v14 has ended by the time of writing this article. For the example NodeJS v18 LTS was used.

CommonJS to ESM conversion steps in detail

ESM modifications to package.json

To switch from CommonJS to ECMAScript/ESM we’ll first make two slight changes to our package.json:

  • Replacing "main": "start.js" with "exports": "./start.js"
    Please note the leading "./" as with ESM every reference to own files/modules has to be a full pathname including the directory and also file extension.
  • Adding "type": "module"
    This is to tell NodeJS we are on ESM now and prevents you having to rename all *.js files in your project to *.mjs as proposed in some guides. Also with ESM we can stay with the *.js file extension as it is still normal JavaScript.
# in package.json

# before
"main": "start.js",

# change to
"exports": "./start.js",
"type": "module",

For more details on exporting the entry point and the module type declaration refer to the official NodeJS documentation for package.json fields.

Replacing require/module.exports with import/export

Now the biggest change has to be done: in all of your source code files you have to replace the statements for exporting and importing as module.exports and require are not longer supported with ESM.

For unnamed exports do the following changes…

// before: CommonJS - unnamed export

module.exports = app;

// after: ESM - unnamed export

export default app;

And for named exports of functions…

// before: CommonJS - named exports

module.exports.route1 = function (req, res) { ... }
module.exports.route2 = (req, res) => { ... };


// after: ESM - named exports

export function route1 (req, res) { ... }
export const route2 = (req, res) => { ... };

Please note the const keyword instead of function when exporting an arrow function. For a complete list of available export statements for other types like arrays, classes, literals and so on please refer to the export statement documentation.

After changing all the exports, let’s move on with replacing require by import…

// before: CommonJS requiring in dependencies

const express = require('express'); // standard module
const app = require('./app'); // own module    
const routes = require('./handlers/routes'); // own module with named exports routes.route1 and routes.route2

// after: ESM importing dependencies

import express from 'express'; // without trailing .js
import app from './app.js'; // with trailing .js
import * as routes from './handlers/routes.js'; // '*' to import all named exports, with trailing .js

Note that own modules must always be imported by providing the path and file extension whereas standard modules installed via npm (like express) don’t.

Importing named exports can also be done selectively if you need only some imports from a module by using destructuring in curly braces like so…

import { route1, route2 } from './handlers/routes.js';

For a full list of available import declarations please refer to the import statement documentation.

You should now already be able to start the migrated ESM project by running npm run start.

Updating the ESLint configuration

Although everything is running, you should notice some ESLint errors in your code…

eslint-module-export-error

This is because ESLint isn’t aware of that you’ve switched from CommonJS to ESM. To fix that simply add the following entries at the top level to your ESLint configuration – in case of the sample project the .eslintrc.json file.

"parserOptions": {
  "ecmaVersion": "latest",
  "sourceType": "module"
},

Now ESLint knows to validate against ECMAScript/ESM syntax with the latest features and no errors should show up any more. For a complete description of the available options have a look at the ESLint parsing docs.

Making Jest working again

Last thing to fix up is running the unit tests with Jest. Invoking npm run test would give you an error like that…

jest-esm-error

To fix this, we change the starting command in our package.json for Jest as suggested in the provided documentation site for ECMAScript modules.

# before: CommonJS Jest start script under 'scripts'

"test": "jest"

# after ESM Jest start script

"test": "NODE_OPTIONS=--experimental-vm-modules npx jest"

Although this feature is still considered being experimental, the tests are now running again.

jest-esm-success

That’s it! The project is now completely converted to ESM.

You may also check out the CommonJs vs. ESM cheat-sheet for further reading.

Happy coding 🙂

Useful links

]]>