loading...

Setting up automated code formatting for Rails using NodeJS

jesalg profile image Jesal Gadhia Updated on ・8 min read

Why?

meme1.jpg

As one of the Rails projects I've been working grew in size in terms of the number of lines of code as well as the number of people contributing code, it became challenging to maintain consistency in code quality and style.

Many times these issues were brought up in code reviews where they should be addressed well before that so the discussion in code reviews can focus on substance and not the style.

So having said that, I wanted to setup an automated way of fixing stylistic issues when the code is checked in. Here's how I went about setting that up.

In my previous blog post, I talked about Incorporating Modern Javascript Build Tools With Rails, if you have something similar setup already, you can skip the next two sections about setting up a NodeJS environment. If not, read on.

NodeJS

Before we can get started, let’s make sure we have NodeJS installed. I’ve found the easiest way to do so is via nvm. Rails developers will find this very similar to rvm. To install, run the following commands which will install nvm and the latest version of NodeJS:

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash
$ source ~/.bash_profile
$ nvm install node
$ nvm use node

Yarn

Next we'll need a package manager. Traditionally we'd use npm but I've found Facebook's yarn to be a lot more stable and reliable to work with. This is very similar to bundler. To install on Debian Linux, run the following commands or follow their installation guide for your OS:

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn

Git Hook Management

meme2.jpg

Now in order to format the code automatically upon check-in, we will have to first figure out how to run those linting scripts. There are a couple of options:

1) Bash Script - You can manually save a bash script as .git/hooks/pre-commit and give it execute permission. Downside of this approach is that you'd have to have every member of your team do this manually. Also if something changes in the script, everyone would have to repeat the process all over again. It would quickly become unmanageable.

2) pre-commit - This is a very robust framework built in Python to manage git hooks. I really like everything about it except the fact that for RoR projects, it adds another language dependency on the local environment in addition to Ruby and NodeJS. Also again this is something the entire team would have to install manually (albeit only once per environment) to get it up and running. I would definitely recommend it for a Python project.

3) overcommit (Recommended) - This is another excellent git hook manager very similar to pre-commit but written in Ruby. It has a ton of built-in hooks for formatting Ruby, JS, CSS and more. It's virtually plugin and play and perfect for a project if it doesn't have a NodeJS build pipeline setup. It will help you avoid introducing another language dependency. Although for the purpose of this blog post, we'll use the next option. I'll recommend checking out this blog post if you want to use this option.

4) husky & lint-staged (Recommended) - These two NodeJS packages act as a one-two punch. Husky lets you specify any script that you want to run against git hooks right in the package.json while lint-staged makes it possible to run arbitrary npm and shell tasks with a list of staged files as an argument, filtered by a specified glob pattern on pre-commit. The best part, once this is setup, your team doesn't have to do a thing other than run yarn install.

To get started install both the packages:

yarn add lint-staged husky --dev

Next add a hook for precommit in your package.json:

{
  "scripts": {
    "precommit": "lint-staged"
  },
}

Lastly create an empty .lintstagedrc file at the root, this is where we'll integrate with the various linters we'll talk about next.

JavaScript

eslint.jpg

So now we are ready to actually hookup some linters. For JavaScript, there are several good linting frameworks out there ranging from very opinionated to very flexible:

1) StandardJS - This is the most opinionated framework out there and also very popular. It has excellent IDE integration and used by a lot of big names. Although having said that, we didn't agree with some of it's rules and there was no way of changing them. It is really designed to be a an install-it-and-forget-it kind of a linter which wasn't quite what I was looking for.

2) Prettier - So that lead me to investigate another very popular framework. Prettier is a lot like StandardJS, good IDE support, well adopted. It tries to provide little more flexibility over a few basic rules compared to StandardJS. Although it's main advantage over StandardJS is the fact that it is also able to lint CSS and GraphQL in additional to JavaScript and it's preprocessors.

3) ESLint (Recommended) - After trying both of the above mentioned linters, I ended up settling with ESLint primarily for the fact that it let us tweak all the options exactly per our needs. The flexibility and extensibility of this framework is impressive.

So let's go ahead and install it:

yarn install eslint --dev

Next you'll want to run through setup and answer some questions about your preferences

./node_modules/.bin/eslint --init

Based on your responses, it will create an .eslintrc file in your project which you can always manually edit later. Here's one that I'm using:

env:
  browser: true
  commonjs: true
  es6: true
extends: 'eslint:recommended'
parserOptions:
  sourceType: module
rules:
  indent:
    - warn
    - 2
  linebreak-style:
    - warn
    - unix
  quotes:
    - warn
    - single
  semi:
    - warn
    - always
  no-undef:
    - off
  no-unused-vars:
    - warn
  no-console:
    - off
  no-empty:
    - warn
  no-cond-assign:
    - warn
  no-redeclare:
    - warn
  no-useless-escape:
    - warn
  no-irregular-whitespace:
    - warn

I went with setting most of the rules as non-blocking warnings since we were dealing with some legacy code and wanted to reduce developer friction as much as possible.

Finally add this line to your .lintstagedrc

{
  "*.js": ["eslint --fix", "git add"]
}

Ruby

rubocop.png

When it came to Ruby linting, there is really just one game in town i.e RuboCop. All you need to do is add it to the Gemfile and run bundle install:

gem 'rubocop', require: false

Next add a hook for it in your .lintstagedrc:

{
  "*.js": ["eslint --fix", "git add"],
  "*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
}

Next you will need to create .rubocop-linter.yml with your coniguration. Here's one that we used:

AllCops:
  Exclude:
    - 'vendor/**/*'
    - 'spec/factories/**/*'
    - 'tmp/**/*'
  TargetRubyVersion: 2.2

Style/Encoding:
  EnforcedStyle: when_needed
  Enabled: true

Style/FrozenStringLiteralComment:
  EnforcedStyle: always

Metrics/LineLength:
  Max: 200

Metrics/ClassLength:
  Enabled: false

IndentationConsistency:
  EnforcedStyle: rails

Documentation:
  Enabled: false

Style/ConditionalAssignment:
  Enabled: false

Style/LambdaCall:
  Enabled: false

Metrics:
  Enabled: false

Also here's a list of all the auto corrections RuboCop is able to do when the -a / --auto-correct flag is turned on if you need to add/change any more rules in that file.

CSS/SCSS

stylelint.png

So now that we have Ruby and JS linting squared away. Let's look into how to do the same with CSS.

1) sass-lint - Since we were using SASS in the project, I first looked at this package. Although quickly realized there was no option for auto fixing available at the moment. There is a PR which is currently in the works that is supposed to add this feature at some point. But for now we'll have to look somewhere else.

2) stylelint (Recommended) - Ended up going with this option because of its large ruleset (150 at the time of writing) and the fact that it is powered by PostCSS which understands any syntax that PostCSS can parse, including SCSS, SugarSS, and Less. Only downside being the fact that auto fixing feature is experimental but it's worth a shot anyway.

So let's go ahead and install it:

yarn add stylelint --dev

Next add a hook for it in your .lintstagedrc:

{
  "*.js": ["eslint --fix", "git add"],
  "*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
  "*.scss": ["stylelint --fix", "git add"]
}

Again this is a very configurable package with a lot of options which you can manage in a .stylelintrc file.

To being with, I'd probably just recommend extending either stylelint-config-standard or stylelint-config-recommended presets.

Here's an example of a .stylelintrc:

{
  "extends": "stylelint-config-standard",
  "rules": {
    /* exceptions to the rule go here */
  }
}

HAML

haml.jpg

As far as templating engine goes, our project uses HAML but unfortunately I couldn't find any auto formatting solution for it. haml-lint has an open ticket for adding this feature but it seems like it's not very easy to implement.

So until then you have the option to just hook up the linter so it can provide feedback about your markup which you would have to manually correct.

To get started, add the gem to your Gemfile:

gem 'haml_lint', require: false

Next add a hook for it in your .lintstagedrc:

{
  "*.js": ["eslint --fix", "git add"],
  "*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
  "*.scss": ["stylelint --fix", "git add"]
  "*.haml": ["haml-lint -c .haml-lint.yml", "git add"],
}

Next you will need to create .haml-lint.yml with your configuration. Here's one that you can use:

# Whether to ignore frontmatter at the beginning of HAML documents for
# frameworks such as Jekyll/Middleman
skip_frontmatter: false

linters:
  AltText:
    enabled: false

  ClassAttributeWithStaticValue:
    enabled: true

  ClassesBeforeIds:
    enabled: true

  ConsecutiveComments:
    enabled: true

  ConsecutiveSilentScripts:
    enabled: true
    max_consecutive: 2

  EmptyScript:
    enabled: true

  HtmlAttributes:
    enabled: true

  ImplicitDiv:
    enabled: true

  LeadingCommentSpace:
    enabled: true

  LineLength:
    enabled: false

  MultilinePipe:
    enabled: true

  MultilineScript:
    enabled: true

  ObjectReferenceAttributes:
    enabled: true

  RuboCop:
    enabled: false

  RubyComments:
    enabled: true

  SpaceBeforeScript:
    enabled: true

  SpaceInsideHashAttributes:
    enabled: true
    style: space

  TagName:
    enabled: true

  TrailingWhitespace:
    enabled: true

  UnnecessaryInterpolation:
    enabled: true

  UnnecessaryStringOutput:
    enabled: true

Optionally, you can also exclude all the existing HAML files with linting issues by running the following command and including the exclusions file (inherits_from: .haml-lint_todo.yml) in the configuration file above to ease the on-boarding process:

haml-lint --auto-gen-config

Conclusion

That's all folks! In a few weeks since hooking up the auto formatters our codebase has started to look much more uniform upon every commit. Code reviews can now focus on more important feedback.

This post was originally published on my blog. If you liked this post, please share it on social media and follow me on Twitter!

Posted on by:

jesalg profile

Jesal Gadhia

@jesalg

Engineering manager & full-stack dev with 10+ yrs of experience building products for major brands & start-ups.

Discussion

markdown guide
 

good article. In terms of ruby, I actually find Reek to be more useful. It critiques OO style. It overlaps rubocop a bit, but I find both have their uses.

 

Right on, didn't think reek had the ability to auto-fix issues which is primarily what I was looking for.