This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Bundling and Automation in Monorepos
  • [00:00 - 00:08] I want to talk about ESLint and the shared configuration that will be setting up. First of all, we should ask ourselves why use ESLint?

    [00:09 - 00:28] The very first reason is that ESLint allows us to enforce agreed upon code quality standards. As a team, we can identify a particular code style or set of preferences and then use ESLint to enforce those decisions consistently. This approach reduces friction among team members.

    [00:29 - 00:49] For example, if there is a specific code quality rule that the team needs to follow, we encode it in an ESLint rule, and that eliminates the need for debates around how it should be done. Another very compelling reason to use ESLint is its ability to leverage TypeScript information to catch additional bugs.

    [00:50 - 00:59] Let's go over an example. If you have a function that returns a promise and you use it in another function, it's very easy to forget to use the await keyword.

    [01:00 - 01:07] This omission could lead to subtle bugs. With typescript-eslint, there is a rule that would catch this.

    [01:08 - 01:22] Moreover, if you intentionally don't want to await a given promise, you can always make that explicit in your code by using the "void" keyword. So the ESLint rule has forced us to be more explicit about the intent of our code.

    [01:23 - 01:52] What we're going to do in this lesson is set up a shared ESLint configuration that works for both ESLint version eight and for ESLint version nine. This is necessary because at the time of recording, Next.js only supports ESLint version eight, but at the same time ESLint eight is now deprecated. You might also have other reasons that make it impossible for you to migrate to ESLint version nine. So I want to show how you can share both version eight and version nine in the same monorepo.

    [01:53 - 02:03] We'll be doing this using ESLint's new flat config format. It's it supported in both version eight and version nine, and gives us the uniform base from which we're going to build.

    [02:04 - 02:23] I'm going to cd into my config package under packages/config, and then I'm going to add ESLint eight's configs as an aliased package. I'm going to call it "@eslint8/js" and alias it to "npm:@eslint/js@^8".

    [02:24 - 02:35] And actually let me quickly rerun this with "--save-exact" so we have an exact version number saved in our package.json. Then I'm going to do the same for ESLint nine.

    [02:36 - 03:07] Doing an aliased package "@eslint9/js" aliased to "npm:@eslint/js@^9" and this needs to be run two times because otherwise pnpm won't actually write the correct exact version. Next I'm going to add the type packages for add "@eslint/js". This is going to look very arcane, but it's necessary to get the correct types for packages that don't ship their own types and also use a namespace like @eslint/js.

    [03:08 - 03:27] I need to repeat this for both ESLint eight and ESLint nine, and unfortunately ESLint nine is a bit weird. We are going to be mapping ESLint nine types to the ESLint eight types because they just haven't shipped version nine types, but luckily the version eight types are still correct for our usage.

    [03:28 - 03:43] The next set of dependencies that we need to add are "typescript-eslint", the "globals" package, and "eslint-config-prettier". I'm now going to create our ESLint version eight configuration file.

    [03:44 - 03:58] I'm going to call it eslint8.base.mjs, and then I'm going to paste in some code that I have prepared ahead of time. @eslint8/js contains the recommended ESLint config that we are going to be extending.

    [03:59 - 04:19] We're going to be using typescript-eslint's config tool to do that, and we're also going to be using it to get some of the defaults from typescript-eslint. The other dependency that we are using is called "globals", and that's used to define what global variables are available in a given environment.

    [04:20 - 04:38] Initially we're just going to set this to the Node environment because our base config is just going to assume Node. But when we have an application that needs to run in a browser, we're going to overwrite this config and then set the correct globals. Finally we'll be using eslint-config-prettier.

    [04:39 - 04:59] What it does is it removes any formatting rules from ESLint so that only Prettier does formatting, while ESLint checks for other types of errors. We're going to be using typescript-eslint's "config()", which is a utility function that allows us to easily combine different configs into one strictly typed flat config file.

    [05:00 - 05:36] Our base config is going to be an extension of the ESLint eight recommended config, followed by typescript-eslint's "recommendedTypeChecked", "stylisticTypeChecked", and finally eslint-config-prettier followed by our overrides. You can name each section of a config, and we're going to be using this because it helps you debug things if something breaks. The important configuration that we need to add is "languageOptions", which is used by typescript-eslint and sets TypeScript as the parser of the files in your project that ESLint is going to be using.

    [05:37 - 05:50] You can look more into this configuration on your own time later. The important part is that we are going to be looking for a TypeScript tsconfig.json file in the directory that we execute ESLint from.

    [05:51 - 06:12] That means that even though our config file lives in packages/config, it's going to be resolving TypeScript configuration from wherever we execute ESLint from. And finally, we have some default ignores. I suggest always ignoring the eslint.config.mjs, which is going to be our config file name for all ESLint configs.

    [06:13 - 06:33] And you should always ignore generated folders like "dist", which we have in our Vite and server applications. And just for posterity, node_modules is already automatically ignored. Now you might notice that the only ESLint eight specific thing in this config is the ESLint eight recommended config that we start with.

    [06:34 - 06:48] That means we can easily extract a shared ESLint config. So let's do that. I'm going to open a file called eslint.shared.mjs, and I'm going to move everything there, that's going to be common between our ESLint eight and ESLint nine configs.

    [06:49 - 07:26] And then I'm going to create an eslint9.base.mjs file, change the import of eslint8/js to eslint9/js, and now we have our ESLint nine config with a shared config that's the base for both of them. This allows us to easily maintain consistent rules across our monorepo, even using different versions of ESLint. With this done, I'm going to go to our apps/frontend-nextjs application I'm going to first edit our package.json to use ESLint directly, rather than using next as a wrapper for ESLint.

    [07:27 - 07:44] Next, I'm going to make sure we have the correct dependencies, which means having "eslint@^8, the "globals" package, "typescript-eslint" and "@eslint/eslintrc", which we're going to use in a moment. Now let's open the .eslintrc.json file.

    [07:45 - 07:54] This is the legacy ESLint configuration format. We'll be migrating it to the flat config format, so I replace it with a new file called eslint.config.mjs.

    [07:55 - 08:11] I've prepared this file ahead of time, so I'm going to paste it in and then we'll go line by line. We have our base configuration coming from @monorepo/config/eslint8.base.mjs. And then we're going to instantiate a FlatCompat object.

    [08:12 - 08:29] This allows us to use plugins and configuration that only work in ESLint eight in our ESLint flat config. The first section of our config is going to be adding an ignore glob for the ".next" folder - we need to remove everything from there.

    [08:30 - 08:51] Then we're going to have our rules section. And I'm going to make use of the "extends" option that's added by "tseslint.config()", and then I'm going to 'compat.extend("next/core-web-vitals")', and our base config comes from @monorepo/config/eslint8.base.mjs.

    [08:52 - 09:08] However, I'm not going to extend the original "next/typescript" config, and the reason for that is that it more or less mimics the config that we already did with typescript-eslint. So we're going to just use that one and not mix it with the one that comes from Next.js.

    [09:09 - 09:25] Finally, because this is a front end application in "languageOptions", I need to set "globals" for browser. Let's test it, and we're going to see a kind of weird failure where two files are not found in the project service.

    [09:26 - 09:40] What does that mean? Well, as I said, we set up our base config to be using TypeScript resolution for parsing files. That means that ESLint can only parse files that TypeScript knows about.

    [09:41 - 09:59] And if we open tsconfig.json, we're going to see that our TypeScript config does not include ".mjs" files. There are two solutions here - we could add these two files to our ignores, but we can also make them parseable by TypeScript, and I prefer the second approach.

    [10:00 - 10:13] I'm going to change our TypeScript includes to parse .mjs files as well. I'll check that TypeScript still passes after this change, and then I'm going to run lint again and see that that passes as well.

    [10:14 - 10:32] With everything working correctly, we can now safely delete .eslintrc.json and we can keep eslint.config.mjs as our main config file. Additionally, we have updated our lint script to run ESLint directly and we have installed all the necessary dependencies.

    [10:33 - 10:48] We have also made sure that TypeScript supports .mjs files. Next, I'll go to our frontend-vite application and we're going to go to a similar procedure. Since Vite already uses a flat config file, this is going to be easier.

    [10:49 - 11:04] I'm just going to name the sections that we have so it's easier to debug if something breaks, and as for the actual configuration, we're going to change the "extents" part. What we're going to do is make it directly extend from our base configuration.

    [11:05 - 11:33] One additional change that I want to make is that the "language" option sets ECMAScript version of 2020, but in the previous lesson, we set our TypeScript to be using ECMAScript 2022. These two should match, so I'm just going to make sure that our ESLint configuration uses 2022 as well. These are all the changes necessary here, and we can now remove the @eslint/js dependency from this app.

    [11:34 - 11:47] Looking at our changes, we now use our shared configuration and we also have removed one unnecessary dependency. Finally, let's go to the server-express app.

    [11:48 - 12:09] I'll open our package.json and add a lint script. Then I'm going to open eslint.config.mjs, and I'm directly going to re-export our default ESLint nine configuration, and finally I'm going to try running linting in here. This is going to fail, but if we look at the code we're going to discover that it's a false positive.

    [12:10 - 12:28] In most cases the rule prefer-nullish-coalescing gives you the desired behavior. But in this particular case, we don't want the port to be set to an empty string, if an empty string is given. We want it to default to 5001. So in this case, we actually want to keep the original code.

    [12:29 - 12:37] We're going to add an inline ESLint disable config. Disabling rules with an inline comment is a best practice.

    [12:38 - 12:58] It ensures that exceptions are clear and justified in code, while at the same time we keep the rule applied globally. After I've made this adjustment, the lint script runs successfully and we can go to the final part, which is to go back to the monorepo root and create an overall lint script in package.json.

    [12:59 - 13:06] We can check that linting now works correctly in our packages. But before we go, we're going to make one last improvement.

    [13:07 - 13:19] I'm going to set all ESLint executions to use the option "--max-warnings=0". In general, your ESLint configuration should never allow warnings.

    [13:20 - 13:29] All rules should be set to error to ensure strict enforcement of coding standards. However, occasionally a misconfiguration might slip through.

    [13:30 - 13:42] By enabling this policy, we ensure that no warnings are accidentally overlooked. With these changes, our Monorepo is now fully configured with consistent linting across all apps.