DEV Community

Cover image for Incremental stylesheet linting with Stylelint and the Nx toolchain
Lars Gyrup Brink Nielsen for This is Learning

Posted on • Edited on

Incremental stylesheet linting with Stylelint and the Nx toolchain

Original cover photo by Carl Raw on Unsplash.

Nx integrates ESLint to our web projects out-of-the-box, but how about linting stylesheets? There are a few stylesheet linters out there, the major ones being scss-lint and Stylelint.

In this guide, we set lint-styles targets using Stylelint for a few projects in an Nx workspace. This way we can lint the styles of one, multiple, or all projects in an automated way and take advantage of Nx computation caching in Nx Cloud and/or locally,

The example workspace can be generated by following the instructions for setting up a new Nx 11 workspace using the empty workspace preset in "The ultimate migration guide to angular-eslint, ESLint and Nx 11".

Adding Stylelint

In this first part, we add Stylelint by following the steps from their Getting started guide.

  1. Install Stylelint and the standard configuration.
    First, install Stylelint and the Stylelint standard configuration.

    Using NPM CLI:

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

    Using PNPM CLI:

    pnpm add --save-dev stylelint stylelint-config-standard
    

    Using Yarn CLI:

    yarn add --dev stylelint stylelint-config-standard
    
  2. Create Stylelint configuration.
    Next, we create a Stylelint configuration at the root of our workspace.

    Create the file <nx-workspace-root>/.stylelintrc with the following content:

    {
      "extends": ["stylelint-config-standard"],
      "rules": {}
    }
    
  3. Try Stylelint.
    To verify that our Stylelint configuration works, we run a Stylelint CLI command from our workspace root:

    Using NPM CLI:

    npx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    

    Using PNPM CLI:

    pnpx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    

    Using Yarn CLI:

    npx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    
  4. Add Stylelint editor extension.
    Stylelint extensions exist for many code editors. See the full list in Editor integrations. For example, the official Stylelint extension for Visual Studio Code is stylelint.vscode-stylelint.

Configuring Stylelint rules

  1. Add Sass Guidelines configuration.
    The Stylelint standard configuration is a good, general purpose lint rule configuration, but in this guide we'll use SCSS.

    Install the Sass Guidelines configuration for Stylelint.

    Using NPM CLI:

    npm install --save-dev stylelint-config-sass-guidelines
    

    Using PNPM CLI:

    pnpm add --save-dev stylelint-config-sass-guidelines
    

    Using Yarn CLI:

    yarn add --dev stylelint-config-sass-guidelines
    

    Now, we add the Sass Guidelines rule configuration to our configuration in .stylelintrc:

    {
      "extends": [
        "stylelint-config-standard",
        "stylelint-config-sass-guidelines"
      ],
      "rules": {}
    }
    
  2. Use Idiomatic CSS ordering.
    If you're an experiened visual frontend developer, you might agree that ordering of CSS properties matter. In this step, we configure Stylelint to follow the Idiomatic CSS conventions.

    First, we install the stylelint-config-idiomatic-order configuration.

    Using NPM CLI:

    npm install --save-dev stylelint-config-idiomatic-order
    

    Using PNPM CLI:

    pnpm add --save-dev stylelint-config-idiomatic-order
    

    Using Yarn CLI:

    yarn add --dev stylelint-config-idiomatic-order
    

    Next, we add it to our Stylelint configuration in .stylelintrc:

    {
      "extends": [
        "stylelint-config-standard",
        "stylelint-config-sass-guidelines",
        "stylelint-config-idiomatic-order"
      ],
      "rules": {
        "order/properties-alphabetical-order": null
      }
    }
    

    Note that we have to disabled the rule order/properties-alphabetical-order as the Sass guidelines configuration enables it, but it conflicts with the order specified by Idiomatic CSS and enabled by the Idiomatic order configuration.

  3. Customize Stylelint configuration.
    We should customize lint rules to our liking. For example, let's adjust our configuration to these preferences in .stylelintrc:

    1. Set the max line length to 80 characters.
    2. Limit allowed selector characters to lower case letters, digits in addition to the hyphen (-) and underscore (_) characters.
    3. Use single quotes (').
    {
      "extends": [
        "stylelint-config-standard",
        "stylelint-config-sass-guidelines",
        "stylelint-config-idiomatic-order"
      ],
      "rules": {
        "max-line-length": 80,
        "order/properties-alphabetical-order": null,
        "selector-class-pattern": "^([a-z][a-z0-9]*)(-_[a-z0-9]+)*$",
        "string-quotes": "single"
      }
    }
    

    Make sure that our configuration still works by running The Stylelint CLI locally.

    Using NPM CLI:

    npx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    

    Using PNPM:

    pnpx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    

    Using Yarn CLI:

    npx stylelint "{apps,libs}/**/*.{css,less,sass,scss,sss}"
    

Automating our Stylelint workflow using Nx

  1. Add lint-styles targets to projects.
    Now it's time to automate linting of our styles. Let's say that we have an application project called booking-app which has a feature library with the project name booking-feature-flight-search. Our global styles are in a project called booking-common-styles.

    Let's first create an execution target for the global styles.

    Using NPM CLI:

    npx json -I -f workspace.json -e "this.projects['booking-common-styles'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'npx stylelint libs/booking/common/styles/src/**/*.scss' } };"
    

    Using PNPM CLI:

    npx json -I -f workspace.json -e "this.projects['booking-common-styles'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'pnpx stylelint libs/booking/common/styles/src/**/*.scss' } };"
    

    Using Yarn CLI:

    npx json -I -f workspace.json -e "this.projects['booking-common-styles'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'npx stylelint libs/booking/common/styles/src/**/*.scss' } };"
    

    Our workspace configuration (workspace.json) now has this project configuration:

    {
      "version": 2,
      "projects": {
        "booking-common-styles": {
          "projectType": "library",
          "root": "libs/booking/common/styles",
          "sourceRoot": "libs/booking/common/styles/src",
          "targets": {
            "lint-styles": {
              "executor": "@nrwl/workspace:run-commands",
              "options": {
                "command": "npx stylelint libs/booking/common/styles/src/**/*.scss"
              }
            }
          }
        }
      }
    }
    

    Note: npx should be pnpx in the command option if we're using PNPM CLI.

    We use the run-commands executor from the @nrwl/workspace package to run Stylelint CLI commands.

    Using NPM CLI:

    npx stylelint libs/booking/common/styles/src/**/*.scss
    

    Using NPM CLI:

    pnpx stylelint libs/booking/common/styles/src/**/*.scss
    

    Using Yarn CLI:

    npx stylelint libs/booking/common/styles/src/**/*.scss
    

    This command runs Stylelint for all scss files in our common booking styles workspace library.

    We can use the same script to add a lint-styles execution target to our feature library project.

    Using NPM CLI:

    npx json -I -f workspace.json -e "this.projects['booking-feature-flight-search'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'npx stylelint libs/booking/feature-flight-search/src/**/*.scss' } };"
    

    Using PNPM CLI:

    npx json -I -f workspace.json -e "this.projects['booking-feature-flight-search'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'pnpx stylelint libs/booking/feature-flight-search/src/**/*.scss' } };"
    

    Using Yarn CLI:

    npx json -I -f workspace.json -e "this.projects['booking-feature-flight-search'].targets['lint-styles'] = { executor: '@nrwl/workspace:run-commands', options: { command: 'npx stylelint libs/booking/feature-flight-search/src/**/*.scss' } };"
    
  2. Add computation caching.
    A great feature of the Nx toolchain is that computation caching can speed up our development workflow by hours and hours saved per month.

    Let's instruct Nx to cache computation results of lint-styles targets using this command:

    npx json -I -f nx.json -e "this.tasksRunnerOptions.default.options.cacheableOperations = [...this.tasksRunnerOptions.default.options.cacheableOperations, 'lint-styles'];"
    

    Our Nx configuration (nx.json) should now have something like these settings:

    {
      "tasksRunnerOptions": {
        "default": {
          "runner": "@nrwl/workspace/tasks-runners/default",
          "options": {
            "cacheableOperations": [
              "build",
              "lint",
              "test",
              "e2e",
              "lint-styles" // 👈
            ]
          }
        }
      }
    }
    
  3. Execute lint-styles targets.
    Now we can lint styles for one project:

    nx run booking-common-styles:lint-styles
    

    we can lint styles for multiple projects:

    nx run-many --target=lint-styles --projects=booking-common-styles,booking-feature-flight-search
    

    we can lint all styles:

    nx run-many --target=lint-styles --all
    

    after adjusting our styles we can rerun projects that failed style linting:

    nx run-many --target=lint-styles --all --only-failed
    

    or we can even lint project styles in parallel:

    nx run-many --target=lint-styles --all --parallel
    

    The output sometimes gets scrambled, so this should be followed by the --only-failed command from above.

    A failed run looks something like this:

    nx run-many --target=lint-styles --all
    
    >  NX  Running target lint-styles for projects:
    
      - booking-common-styles
      - booking-feature-flight-search
    
    ———————————————————————————————————————————————
    
    > nx run booking-common-styles:lint-styles 
    
    libs/booking/common/styles/src/lib/generic/_document.scss
     8:3  ×  Unexpected empty line before declaration   declaration-empty-line-before
    
    ERROR: Something went wrong in @nrwl/run-commands - Command failed: npx stylelint libs/booking/common/styles/src/**/*.scss
    
    ———————————————————————————————————————————————
    
    >  NX   ERROR  Running target "lint-styles" failed
    
      Failed projects:
    
      - booking-common-styles
    
  4. Add lint styles scripts.
    To support our workflow, we add lint-styles and affected:lint-styles scripts to package.json:

    {
      "scripts": {
        "affected:lint-styles": "nx affected --target=lint-styles",
        "lint-styles": "nx run-many --target=lint-styles --all"
      }
    }
    

    Note that the affected command is based on implicit dependencies configured in nx.json as well as dependencies detected because of EcmaScript imports.

Conclusion

This guide demonstrates how easy it is to integrate and automate tools with the Nx toolchain.

First, we added Stylelint and a few style lint configurations for demonstration purposes. We also customized the style lint rules to our liking.

Next, we added lint-styles targets to our frontend projects by using the @nrwl/workspace:run-commands executor.

Finally, we enabled computation caching and added scripts for linting all styles and affected styles.

This is all we need for both local development and for a deployment pipeline. In addition, everything in this guide applies to all frontend projects, whether they use Angular, React, Svelte, Vue, other frameworks, or vanilla.

Caveats

Keep in mind that we need styles in separate files for Stylelint to work. This means that we can't use inline styles in for example Angular components.

The affected command is based on implicit dependencies configured in nx.json as well as dependencies detected because of EcmaScript imports. This means that affected:lint-styles might not always be accurate.

Next steps

The next natural steps enabled by the Nx toolchain are to:

  • Create a Stylelint executor instead of using the run-commands executor.
  • Create a Stylelint init generator enabling something like nx add <package-name>.
  • Extending a project generator with an option to add a lint-styles target.
  • A generator to add a lint-styles target to a specific project.
  • Create a generator which adds lint-styles and affected:lint-styles scripts.

These are all known as Nx plugins.

Let me know if you accept this challenge.

Top comments (9)

Collapse
 
cjhaviland profile image
CJ Haviland • Edited

Thanks for this article! It helped me out a lot, but I did want to point out your regex expecting your selectors to be "some-thing" rather than expecting a dash OR an underscore. I ended up doing ^([a-z][a-z0-9]*)((-|)[a-z0-9]+)*$.

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

Thanks, CJ 👍

Collapse
 
phillip9587 profile image
Phillip Barta

I wrote a nx plugin which provides this features.

Please check it out: github.com/Phillip9587/nx-stylelint

Nx Stylelint provides a set of power ups on Nx to lint your projects with Stylelint.

  • Executor: Provides a executor to lint your styles with stylelint.
  • Only Affected: Uses Nx to only lint affected projects.
  • Cache: Uses the Nx cache to cache already linted projects.
  • Generators: Automatically add a stylelint configuration and target to your projects.
Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

Very nice, Phillip! 👏

Collapse
 
jeyathilak profile image
David Jeyathilak Sundersingh

nx run-many --target=lint-styles --all runs with no errors. But, if I run npx lint-staged as part of husky pre-commit hook, it gives lint style errors. Have you made list-staged work with the same rules and config as stylelint?

Collapse
 
loclv profile image
LuuVinhLoc

Thank you for your article! It saved me a lot of time!

You can use stylelint cache,
to improve stylelint's speed. 😄

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

Thanks for your feedback and for the caching tip

Collapse
 
fkrautwald profile image
Frederik Krautwald

I took your challenge and wrote a simple Stylelint executor. I’ll try to add an init generator when I find some time.

Thanks for another great write-up.

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

Thanks for letting me know, Frederik. You're very welcome to submit a PR to nxworker for these features.