DEV Community

Cover image for Set up linting and formatting for code and (S)CSS in Next.js with ESLint, Stylelint, Prettier and lint-staged
nausaf
nausaf

Posted on • Updated on

Set up linting and formatting for code and (S)CSS in Next.js with ESLint, Stylelint, Prettier and lint-staged

Next.js CLI generates a really nice, minimal app scaffold with the production features that it is famous for, including a production-grade Webpack config, static site generation and support for SSR and SEO. However, the scaffold lacks a proper linting and code formatting setup.

In this post I'll show you how I configure linting and formatting in my Next.js projects. Setup shown is for a TypeScript app. However all of the steps would remain the same for a Javascript app except for one change that I'll point out in due course.

I set up the following:

  • Prettier for code formatting This ensures that your codebase is consistently formatted. Prettier can format pretty much any file (.html, .json, .js, .ts, .css, .scss etc.).

  • ESLint for linting code files: ESLint lints code files (Javascript and Typescript) for errors and problems and enforces coding style.

  • Stylelint for linting stylesheets Stylelint lints stylesheets (CSS and SCSS etc.) for errors and problems, including use of deprecated syntax and enforces coding style.

  • Scripts in package.json The most imporant thing here is to ensure that linters are part the "build" script that is used to build the app locally and in CI/CD pipelines (including when deploying to Vercel, the company behind Next.js).

  • lint-staged to run linters and code formatter on staged files in a Git pre-commit hook so that linters and formatter automatically run whenever we execute git commit.

    This is a fast failsafe to ensure code doesn't get committed if it fails linting rules and that when it does get committed, it is consistently formatted.

    Running linters only on staged files (those that have been added to Git index using git add) is much faster than processing all files in the working directory.

  • VS Code extensions to run Prettier, ESLint and Stylelint as you edit files. You can see any linting errors in real time via syntax highlighting and the code gets formatted properly as soon as you have made your changes. This is very useful if you use VS Code.

To follow along, generate a Next.js app using npx create-next-app myapp --eslint --typescript. Then open up the new app in your code editor and proceed as follows.

Prettier setup

Prettier is an opinionated code formatter that can format pretty much any file (.html, .json, .js, .ts, .css, .scss etc.).

Set it up as follows:

  1. Install Prettier:

    npm install --save-dev prettier
    
  2. Create .prettierrc.json in project root. Mine has the following content (the defaults for all other settings work fine for me):

    {
      "singleQuote": true,
      "jsxSingleQuote": true
    }
    
  3. Create .prettierignore

    node_modules
    .next
    .husky
    coverage
    .prettierignore
    .stylelintignore
    .eslintignore
    

    This file ensure that files which are not app code (i.e. which are not .js, .ts, .css files etc.) do not get formatted, otherwise Prettier would end up spending too much time processing files whose formatting you don't really care about.

    'prettierignore (the file we just created), .eslintignore and .stylelintignore have been ignored because these are plain text files with no structure so Prettier would complain that it cannot format them.

ESLint setup

ESLint is the linter for JavaScript/TypeScript code.

This is the ESLint config that is generated by Next.js CLI:

{
  "extends": "next/core-web-vitals"
}
Enter fullscreen mode Exit fullscreen mode

If your app uses JavaScript then the default ESLint config is fine and you can skip the rest of this section.

Before we set up ESLint for use in a TypeScript app, note the following:

  • In tsconfig.json in app root, compilerOptions.strict is already set to true:

    {
      "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        strict: true,
        ...
    

    This enables TypeScript's strict code checking. This means that if we run npm run build (the "build" script is defined as next build in package.json), the command would fail if there's an error in the TypeScript code under a strict enforcement of TypeScript's type safety rules.

    However, npm run lint (the "lint" script is defined as next lint in package.json, which in turn runs ESLint) would still succeed. This happens because as configured by Next.js CLI, ESLint does not enforce TypeScript's strict type safety rules.

    This is an issue, not least because linting staged files with lint-staged when committing code to Git would not fail even if there are strict type safety errors in it, and so the code would get checked in.

    Therefore we need to modify ESLint config to enforce TypeScript's strict checking during linting also.

  • Since Next v13, scaffolded projects are configured with a TypeScript Plugin demonstrated in this video. However, at least as of 05/05/2023, contrary to what their docs state, this provides edit time intellisense and syntax highlighting only. If the rules that the plugin looks out for are violated, neither linting (command next lint) nor building (next build) fails.

    Therefore this plugin does not remove the need to set up ESLint properly for TypeScript.

To set up ESLint, proceed as follows:

  1. On terminal in the app directory, run:

    npm install --save-dev eslint eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
    
  2. If you already have a .eslintrc.json in app's root folder, rename it to .eslintrc.js.

    Otherwise create a file named .eslintrc.js in the root.

  3. Replace contents of eslintrc.js with the following:

    /* eslint-env node */
    module.exports = {
      root: true,
      parser: '@typescript-eslint/parser',
      plugins: ['@typescript-eslint'],
      extends: ['eslint:recommended', 'next/core-web-vitals', 'prettier'],
      overrides: [
        {
          files: ['*.ts', '*.tsx'],
          parserOptions: {
            project: ['./tsconfig.json'],
            tsconfigRootDir: __dirname,
          },
          extends: [
            'plugin:@typescript-eslint/recommended',
            'plugin:@typescript-eslint/recommended-requiring-type-checking',
    
            //declaring 'next/core-web-vitals' and 'prettier' again in case
            //the two plugin:... configs above overrode any of their rules
            //Also, 'prettier' needs to be last in any extends array
            'next/core-web-vitals',
            'prettier',
          ],
        },
      ],
    };
    

    This is what the various lines of this file do:

    /* eslint-env node */ stops ESLint from complaining that this is a CommonJS module. We have had to put this in because ESLint, as we have configured it here, does not allow CommonJS modules (which .eslintrc.js is, see module.exports = ... at the top) in a TypeScript app and expects modules in the project to be ES6.

    root: true says this is the topmost eslint configuration file although there may be nested eslint configs in subfolders.

    parser: '@typescript-eslint/parser' specifies the ESLint Typescript parser to be used instead of the default Espree parser.

    parserOptions: tells the parser where to find the tsconfig.json file.

    extends: specifies a number of ESLint configurations, each of which is a bundle of linting rules. A rule in a config further down the array (further to the right) overrides the same rule if declared in any config to the left.

    Config 'next/core-web-vitals' specifies Next.js-specific rules (this was the only config in the original .eslintrc.json generated by the Next.js CLI).

    Config prettier is provided by the package eslint-config-prettier that we installed earlier and switches off those rules in ESLint that conflict with the code formatting done by Prettier. This must be the last config in extends:.

    plugins: ['@typescript-eslint'], specifies the plugin which provides the parser as well as two of the configs that provide Typescript-specific linting rules.

    The overrides section ensures that strict Typescript linting rules only apply to files with .ts and .tsx extensions (from this excellent StackOverflow answer). Otherwise, if we had put the configs in extends that are currently in the override in the root extends instead, they would apply to .js files also. Then running ESLint on the project would throw errors on .js files if they don't meet strict Typescript rules. This would be a problem as we have several .js config files including the .eslintrc.js itself, so there would be linting errors. These errors are avoided by the use of the override.

  4. Create .eslintignore in the project root. It doesn't need to have any content for now:

    
    

    but would come in handy in the future if ever you need to add folders or files that should be ignored by ESLint (see the final section of this post for an example).

Stylelint setup

Stylelint is a linter for CSS and SCSS stylesheets (as well as for stylesheets in other formats).

The setup given below would work for both CSS and SCSS files:

  1. On the terminal in project root:

    npm install --save-dev sass
    

    Next.js has built-in SASS/SCSS support (so the webpack config knows how to handle .scss and .sass files). However, you still need to install a version of sass package yourself, which is what we did above.

  2. Install packages for Stylelint and its rule configs:

    npm install --save-dev stylelint stylelint-config-standard-scss stylelint-config-prettier-scss
    

    Of these three packages:

    stylelint is the linter.

    stylelint-config-standard-scss is a Stylelint config that provides linting rules. It uses the Stylelint plugin stylelint-css and extends configs stylelint-config-standard which defines rules for vanilla CSS, and stylelint-config-recommended-scss which defines SCSS specific rules. As a result extending from this one config is enough to get linting support for both CSS and SCSS files.

    stylelint-config-prettier-scss extends stylelint-config-prettier and turns off those Stylint rules that conflict with Prettier's code formatting. This should be declared last in extends: array in .stylelintrc.json (as shown below).

  3. Create .stylelintrc.json in project root with the following contents:

    {
      "extends": [
        "stylelint-config-standard-scss",
        "stylelint-config-prettier-scss"
      ],
      "rules": {
        "selector-class-pattern": null
      }
    }
    

    "extends" section declares the two Stylelint configs whose NPM packages we installed in the previous step.

    "rules" section is used to configure stylints rules. Here you can turn on or off, or configure behavior of, individual Stylelint rules. A rule can be turned off by setting it to null, as I have done for "selector-class-pattern". I turned it off because it insists on having CSS classes in the so called kebab case e.g. .panel-quiz instead of .panelQuiz. I find it inconvenient for various reasons so I turned it off.

  4. Create .stylelintignore in the project root with the following contents:

    styles/globals.css
    styles/Home.module.css
    coverage
    

    I create this file so that the two stylesheets generated by Next.js CLI which do not comply with the linting rules can get ignored (there might be a better way of doing this but this works for me). Also, files in coverage folder do not need to be linted and would likely throw up errors.

Set up package.json scripts

  1. The most important script is "build". The default command for this script, next build, runs ESLint but not Stylelint. So modify it in package.json file as follows:

    {
      "scripts": {
        "build": "prettier --check . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next build",
        ...
    

    Using this tweak, we can run npm run build locally or in a CI/CD pipeline and it would fail not only on ESLint failure but now also on Stylelint failure. Indeed if you deploy your app to Vercel, the default build logic there also calls npm run build. So when I introduced an error in one of my stylesheets, then deployed to Vercel, I got the following error during deployment:

    Image description

    Notice we have added prettier --check . at the front also. This means if a file is not formatted as per our Prettier setup then too the build script would fail. This should not happen ordinarily (see lint-staged setup below) but is a failsafe to prevent inconsistently formatted files from creeping into the codebase. If there are exceptions you want to allow, you can always mark those using special comments that Prettier understands (discussed below).

  2. Set up "lint" and "format" scripts in package.json. These allow formatting and linting on the command line and come in handy every now and then:

    {
      "scripts": {
        ...
        "lint": "prettier --check . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next lint",
        "format": "prettier --write ."
    
  3. I recommend setting up a build:local script as follows:

    "build:local": "prettier --write . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next build",
    

    The only difference between this and the build script above is that this formats the code before linting and building it instead of just --checking the formatting. This is really useful when coding because tools such as command line tools and VS Code extensions frequently generate or alter code so that formatting is no longer consistent the Prettier ruleset. When this happens, it is quite frustrating to run npm run build only to find out that Prettier has failed because some files require formatting.

    If locally, during development, we run npm run build:local instead of npm run build, any files that need formatting would first get formatted before lint and build takes place. Therefore the build will never fail because of a formatting issue in a file that we did not (manually) introduce.

    The reason why I have separated this from build script is because build is used on Vercel during remote build and deployment process. For this reason I use npm run build in my own CI/CD pipelines on GitHub Actions and Azure DevOps also. Hence the need to define another command for local use only that actually formats files instead of only checking the formatting.

lint-staged setup

lint-staged is a package that can be used to run formatting and linting commands on staged files in a Git repo. Staged files are those that have been added to the Git index using git add ., these are the files that have changed since last commit and will get committed when you next run git commit.

Husky is the typical choice in Node.js packages for registering commands to run in Git hooks. For example, registering npx lint-staged with Husky to run in the Git pre-commit hook means lint-staged will run automatically whenever we execute git commit to commit our code. At that time, the formatter (Prettier) and linters (ESLint and Stylelint) that have been configured in lint-staged will run on the staged files. If there are any errors, the commit would fail.

Whenever git commit fails due to linting errors, we can fix those, then run git add . and git commit again. Thus code only ever gets into the repo after it has been consistently formatted and verified to be free of linting errors. This a particularly big advantage in a team setting.

I prefer to run formatting and linting only on staged files using lint-staged rather than on the entire codebase. This is both much faster (a much smaller number of files needs to be processed) and more useful (there's no point running formatter on files that are in the local directory but which haven't been staged, because any changes introduced by the formatter won't get committed).

Setup for lint-staged and Husky is as follows:

  1. Install lint-staged package:

    npm install --save-dev lint-staged
    
  2. Create lint-staged.config.js in project root with the following contents:

    /* eslint-env node */
    const path = require('path');
    
    const eslintCommand = (filenames) =>
      `next lint --file ${filenames
        .map((f) => path.relative(process.cwd(), f))
        .join(' --file ')}`;
    
    const formatCommand = 'prettier --write';
    const stylelintCommand = 'stylelint --allow-empty-input "**/*.{css,scss}"';
    module.exports = {
      '*.{js,jsx,ts,tsx}': [formatCommand, eslintCommand],
      '*.{css,scss}': [formatCommand, stylelintCommand],
      '!*.{js,jsx,ts,tsx,css,scss}': [formatCommand],
    };
    
    

    The above file configures lint-staged to run Prettier, Stylelint and ESLint (next lint command invokes ESLint).

    lint-staged runs all three commands (see the object assigned to module.exports) in parallel. Therefore the globs for the commands need to select sets of files that are disjoint (do not overlap). Otherwise there could be a race condition where mutiple commands run on the same file at the same time with indeterminate results.

  3. Install Husky NPM package.

    npm install --save-dev husky    
    
  4. Create and run a prepare script to run Husky's own husky install command:

    npm pkg set scripts.prepare="husky install"
    npm run prepare
    

    The "prepare" script is a lifecycle script and one of the conditions under which runs automatically is when npm install is run without arguments in the project folder. Defining this script as we have done ensures that if you or a teammate git fetches the repo and then runs npm install in the project folder to fetch all its NPM dependencies so that you may start work on the project, then Husky's own husky install command would also run. This would set Husky up to run any defined Git hooks when Git operations are performed in the project folder.

    Since the npm prepare script hasn't run yet, we run it for the first time to register Husky with the local Git repo, hence the command npm run prepare at the end of the snippet above.

  5. Register lint-staged with Husky to run at Git pre-commit:

    npx husky add .husky/pre-commit "npx lint-staged"
    

VS Code extensions setup

If you use VS Code as your code editor:

  1. Install the following VS Code extensions to provide linting and formatting on file save and syntax highlight on linting errors:

    a. ESLint extension

    b. Stylelint extension

    c. Prettier extension

  2. Put the following in a settings.json file in .vscode folder in the project (you can of course put these settings in you User Preferences file also).

    {
      "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[javascriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[scss]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "stylelint.validate": ["css", "scss"],
      "editor.formatOnSave": true,
      "editor.codeActionsOnSave": {
          "source.fixAll": true
      },
    }
    
    

As set up, the extensions would lint and format on Save.

Final Checks and Troubleshooting

Build and commit:

npm run build
git add .
git commit -m "fix: set up linting and formatting" 
Enter fullscreen mode Exit fullscreen mode

Building and committing is a good sanity check for the setup we just did:

  • If anything had not been set up correctly, you would likely get errors either during build or at commit.

  • If you already had some code in the project, then there might be a few errors when you commit. Typically, these can be resolved by:

    • adding folders or file to one of the *ignore files.

      For example, I already had some code in my project with Storybook installed. So I had to add folders .storybook and storybook-static to each of .stylelintignore, .eslintignore and .prettierignore as all three tools complained about them.

      stories
      storybook-static
      
    • Adding plugins for specific file types.

      For example, I had Gherkin .feature files in my project to describe integration tests. Prettier couldn't format these. So I added the prettier-plugin-gherkin by simply running:

      npm install  prettier-plugin-gherkin --save-dev
      

      Note that usually it is enough to install the package for a Prettier plugin for Prettier to locate it and additional configuration is not required.

      Likewise, ESLint complained when it encountered .cy.ts files containing Cypress interaction tests for my app. To resolve this linting error, I installed the NPM package for Cypress ESLint plugin and configured it as described here (unlike Prettier, to get this ESLint package to work, some configuration was required).

    • Sometimes it is safe to turn off a linting rule at a specific line or for a whole file. While I am always wary of doing this, in a (deliberately bad) experimental code file, I had many instances of an error that VS Code ESLint extension pointed. This was not caught before but was now being pointed out because strict TypeScript linting rules had been enabled:

      Image description

      So I pressed Ctrl + . to Show Code Actions (I could instead have clicked the yellow lighbulb icon shown next to the issue), then selected Disable @typescript/no-non-null-assertion for the entire file.

      Image description

      This placed the comment /* eslint-disable @typescript-eslint/no-non-null-assertion */ on top of my file to disable all instances of that particular error within the file:

      Image description

Top comments (2)

Collapse
 
parnasmi profile image
Ilhom Max

Really useful article with instructions. I followed line by line and setup configuration for our enterprise app. Everything is working OK. Thank you!

Collapse
 
nidala1992 profile image
nidalA1992

Thank you. It was very helpfull for me!