Linting Meteor Applications

Getting immediate feedback and ensuring consistent code style

This article will introduce you to code linting. It shows how linters are useful, how to set them up and how to integrate them into Meteor projects, Continuous Integration and your editor. It also introduces ESLint-plugin-Meteor, a tool I created specifically for linting Meteor projects.

Meteor 1.2 was released a while ago. One of its new features is meteor lint. You can try it out by running the following commands in any Meteor 1.2 project.

$ meteor add jshint
$ meteor lint

jshint is a new Atmosphere package by the Meteor Development Group. jshint wraps the npm module JSHint, the static code analysis tool. jshint works together with meteor lint to check your project for issues. Here’s what that looks like:

Linting a fresh project with `meteor lint`

Wait, wasn’t there an error?

Yep, there was. jshint warned about the undefined variables Session and Template. JSHint thinks these variables are undefined because it doesn’t know anything about Meteor and its implicit globals.

jshint reporting some errors

Linters are designed to be able to lint any file given that files source-code, some rules and some options.

Rules

Rules tell the linter what it should check for. Rules can have different aspects: There are rules that 1) warn of possible errors, 2) make sure best practices are followed or 3) warn about stylistic issues.

Options

Options can tell the linter about the environment the file is in. Is this file going to run in the browser? Or on node? Is it a Meteor file? Should it support JSX? This matters because it determines whether the given file is valid or not. For example, using window.location would be an error in node, but not in the browser. Options can also tell the linter about globals defined in other files, like Session and Template.

Great! So now we simply add Template and Session as globals to the options and the linter will succeed, right? Well, it would.

But,

Session is actually only valid on the client. If the file is running on the server it would throw an error, even though the linter told us it would be okay. That’s bad because the linter is not doing its job! But that is our fault because we gave the linter wrong information by defining Session as a global. What we really need are globals on a per-file basis. And since Meteor runs some files on the server and some on the client, the different environments have to be taken into context as well. But even on a universal file, Session wouln’t be allowed within a if (Meteor.isServer) block. This gets complicated really fast.

Meteor Development group thought of this. jshint actually uses the new Build Plugins API. As you may have seen in the first gif, meteor lint actually builds your app before running jshint. It does this to determine all the globals that are going to be available to every file being linted. jshint could now mark all global variables as defined before running the linter. This enables linting the whole project without defining all the globals manually. That’s amazing, right?

No,

the point of linting is to get immediate feedback during development. Building the whole project before getting any response takes too long. That’s why MDG runs the lint build plugins along with your app during development.

When the code is changed, the linter runs in the build process.

Notice that the same file is linted twice, once in the context of a server and once in the context of the browser. This is necessary because Meteor shares some files between these environments.

The recording above shows the linter running on every file save.
That’s amazing, right?

No.

Having to go to the terminal after every saved change is cumbersome. Even having to save the file before getting feedback is too much work.
We want feedback. And we want it immediately.

There’s no way we’re going to get immediate feedback if the whole app has to be rebuilt every time we change something. Luckily, this is a solved problem. The proven way is to ensure every file can be linted on its own. This works by using ES6 modules instead of having functions and variables magically available through the global namespace.

In ES2015 great care was taken to make modules statically analyzable. By defining imports explicitly, it is possible to lint any file without building the whole app first. This is what enables the immediate feedback.


So?

Check out the recording below. It shows me typing some code in my editor. And I get immediate feedback, right there. And it even knows about Meteor’s API 🎉

Immediate feedback

The code tries to use Template.bar.rendered which is deprecated in favor of Template.bar.onRendered. The rule meteor/no-blaze-lifecycle-assignments warns us about this, right in the editor and even before saving the file. In fact the linter is so fast it even warned while typing Template.bar.

How to achieve this?

This is done by not relying on an Isobuild plugin (like jshint) and using ESLint directly instead. ESLint is a very extensible linter. Almost everything can be plugged into ESLint. Integrations for every major editor exist which display reported errors during development. ESLint enables you to lint like it’s 2015… so you’re a year behind if you’re not using it yet 😀

To actually benefit from the amazing things linters offer, developers will have to set up their editors and projects accordingly. So, here’s how to do it.

Setting up the editor

This section is going to show the instructions for Atom, but ESLint is integrated into many other editors as well: Available Editor Integrations.
The only catch is that you have to make sure the editor uses the locally installed ESLint (from “./node_modules”) instead of its own or a globally installed one.

Let’s start by adding the packages linter and linter-eslint. You can use the command-line to install packages with Atom’s package manager.

$ apm install linter
$ apm install linter-eslint

Alternatively, you can do so using the UI. Now, restart Atom, open the project and you’re almost good to go! Next, ESLint needs to be enabled in the project.

Setting up the project

Installing packages

Make sure you have installed npm. At the root of your Meteor project, run the following commands. You can simply go with the default values for everything npm init asks you about.

$ npm init --yes
$ npm install eslint --save-dev

This will initialize npm in your project by creating package.json. This is the control file for npm. The second line installs ESLint. The save-dev option tells npm to save eslint as a development dependency in package.json. This enables anybody collaborating on the project with you to simply do npm install after cloning your repository and they’ll have ESLint available at the same version.

Note: Since these npm modules are not used within the Meteor application, this will work without meteorhacks:npm and also with versions before Meteor 1.3. 
We install the modules as dev-dependencies for the same reason, so they don’t end up in the Meteor bundle. Since we’re not using “meteor lint” this even works with projects below 1.2.

You will get the following warnings after npm install eslint -save-dev.

$ npm WARN EPACKAGEJSON meteor-project@1.0.0 No description
$ npm WARN EPACKAGEJSON meteor-project@1.0.0 No repository field.

That’s because having a package.json file in your Meteor project makes that folder a npm package. Packages must have certain fields filled out in their package.json. You can get rid of this warning by adding private: true to your package.json. This tells npm to never publish this folder to npm, so you can’t share your private source code by running npm publish accidentally. Now that the package is declared as private, the warnings will disappear as well, because private packages don’t need to have these fields filled out. Run the following commands to add another package.

$ npm install eslint-plugin-meteor --save-dev

This installs eslint-plugin-meteor, a plugin for ESLint I created. It provides rules specifically for Meteor. This will make ESLint aware of the Meteor API.

Great, ESLint has now been installed to node_modules/eslint. It can be run with this command:

$ node_modules/eslint/bin/eslint.js

But this is a little bit hard to remember. Npm scripts export the executables, so their commands can reference them without the full path. Add the following to your package.json

"scripts": {
"lint": "eslint ."
}

Now, you can lint the project with

$ npm run lint

Any script defined in package.json runs shell commands. Modules installed into node_modules (like eslint) will be available without referring to them through their long path. Having all of the development dependencies installed at a fixed version through npm and using them through npm ensures a consistent development environment in teams. It also ensures the Continuous Integration uses the same version of packages, because the CI can simply run npm install before testing the project.

Configuring ESLint

Now, ESLint needs to be told about the plugin and your project. Create a dot-file called “.eslintrc.json” at the root of the project and add the contents below.

Enabling ES2015

If your project uses ES6, ESLint has to be told about that. Otherwise it will fail with a Parsing Error.

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

Adding some rules

Finally, add some rules for the project. Let’s forbid the use of semicolons as an example. All available rules of ESLint can be found here.

{
"rules": {
"semi": [2, "never"]
}
}

Every rule can be configured to one of three modes:

  • “off” or 0 — turn the rule off
  • “warn” or 1 — turn the rule on as a warning (doesn’t affect exit code)
  • “error” or 2 — turn the rule on as an error (exit code is 1 when triggered)

The semi rule further accepts another option “never” which tells it to never allow semicolons. It could also be switched to “always” to enforce usage of semicolons everywhere.

The complete options for rules are explained in the ESLint documentation.

Adding the Meteor environment

There is a rule called no-undef which warns whenever the code tries to access an undefined variable. Since ESLint doesn’t know about Meteor’s API by default, it would throw an error when we’re using Meteor.user() or any other Meteor global. We’ll have to tell ESLint about all these globals. This could be done like this:

{
"rules": {
"no-undef": [2]
},
"globals": {
"Meteor": false,
"Session": false,
// and so on..
}
}

Because it’s tedious to tell ESLint about all the variables, ESLint comes with environments which are predefined sets of globals. The Meteor environment can be enabled like this:

{
"env": {
meteor": true
}
}

Because Meteor code can run in the browser and on node, we should enable these environments as well:

{
"env": {
"meteor": true,
"node": true,
"browser": true
}
}

Meteor and Session can now be removed from the globals, because they will be provided through the meteor environment. Now the linter will know about all the predefined globals your application could use. If you create a custom collection, you’d add it as to globals like we did with Meteor and Session above.

Note: Since ESLint isn’t aware of Meteor’s conventions of which file is going to run on the client or the server, it won’t warn when client-only variables are used on the server and vice versa, e.g. Session on the server. I tried to work around this in the first version of ESLint-plugin-Meteor, but it turned out to introduce a lot of complexity and configuration overhead for mistakes which are very unlikely to happen. So I dropped it again in ESLint-plugin-Meteor v2.

Adding Meteor specific rules

Rules built specifically for Meteor are enabled through eslint-plugin-meteor. First, the plugin itself has to be enabled in .eslintrc, then individual rules can be turned on.

{
"plugins": ["meteor"],
"rules": {
"meteor/no-template-lifecycle-assignment": 2
}
}

Here, meteor refers to eslint-plugin-meteor. This is a shorthand which can be used for all ESLint plugins, for example react would refer to eslint-plugin-react.

The rule above warns when the deprecated method of assigning template lifecycle callbacks is used, so the following will fail:

Template.foo.rendered = function () {}

When changing it to the new syntax, the error disappears:

Template.foo.onRendered(function () {})

This example shows how Meteor specific rules for ESLint can warn of deprecated APIs. You can check out all available rules of eslint-plugin-meteor here.

ESLint-plugin-Meteor comes with a recommended set of rules. To use that preset, add it as an extension.

{
"plugins": ["meteor"],
"extends": ["plugin:meteor/recommended"]
}

Note: Using extends for rules of plugins will only work with ESLint v2 and newer.

Enabling Continuous Integration

A popular way to test projects is using npm’s test script. The special pretest script will run before any tests are executed. This is the ideal place to run the linter.

{
"scripts": {
"lint": eslint ."
"pretest": "npm run lint",
"test": "<your test command>"
}
}

Now, all your CI has to do is execute npm tests and you’re good to go!
An example of this can be seen here.

ESLint-plugin-Meteor

The Meteor Guide has been released recently. It offers best practices for Meteor projects. The linter is a perfect place to ensure you are following them, like not using Session, auditing arguments, having all template names in camel-case and many more.

If you want to learn more about the ECMAScript specification, compilers and open source, then you should contribute by implementing some rules. I’ll try my best to help you contribute your own rule to ESLint-plugin-Meteor. Simply open an issue stating you want to help, and I’ll get in touch.

Example configuration

I have created a gist containing the bare minimum files I’d recommend as a base for your project. It uses ESLint with ESLint-plugin-Meteor. It is built on top of AirBnB’s JavaScript style guide.

ESLint supports configuration files in different formats. Up until now, JSON was used. This gist uses YAML since it’s more readable for configuration files. Every time you add an Atmosphere package, tell ESLint about its exported globals by adding them to .eslintrc.yml. The same goes for collections.

Check the gist out.

I also sent two PRs to popular repos showing off how to integrate ESLint-plugin-Meteor: meteor/todos and wekan/wekan.

meteor lint?

As you may have noticed, the project is actually never using meteor lint.

Is meteor lint still useful then? That question is better rephrased as Are Isobuild Lint Packages still useful?, because meteor lint does little beside running them. A proper linting package can be very useful. It would not make use of any of the magic provided by Isobuild (global variables, known architecture). It has to adhere to this restriction to support consistent linting results with the editor. Anything that is valid in your editor should be valid in meteor lint, and vice versa. 
The true value of a proper atmosphere-based linting package is that it could define a code standard the whole community can follow by simply running

$ meteor add whoever-creates-it:standard

This standard package could define a common code style, much like standard by @feross does in the npm world.

Working around Meteor’s missing module support

Globals are bad. Unfortunately Meteor uses them a lot.
Meteor does not support ES6 modules yet, so the code has to be made statically analyzable another way. To do this, hints could be given to the linter through comments and configuration. This results in a lot of configuration work for little benefit. When Meteor will finally support modules in 1.3, most of these hints can be removed and everything will continue to work.

Setting up your teammates

After one of your colleagues clones the git project, he only needs to run npm install and npm will install all the packages listed in packages.json, including the linter. When the teammate has set up his editor correctly, he’s good to go :-)

Wrapping up

Linting is awesome. To keep it awesome, we should not rely on Isobuild magic as it will slow down feedback cycles and breaks existing tooling in editors. Instead of creating complex tools that understand Meteor, the Meteor code should be simplified so it can be understood by existing tools. The tooling support for JavaScript is great and using Meteor should not break it.

Having a linter in your editor will shorten your feedback-cycles drastically. Having a linter run in your Continuous Integration will ensure code quality stays top-notch and reduce errors.

Happy Linting!


If you don’t want to miss updates for ESLint-plugin-Meteor, follow @dferber90 on Twitter. I tweet about Javascript, Meteor and new web technologies.


Sidenote: This article was originally written almost 6 months ago. ESLint-plugin-Meteor has since been undergoing heavy changes and its focus shifted on best-practices instead of ensuring correct Meteor API usage. ESLint-plugin-Meteor previously tried to determine where files are going to run (client, server, both) and provide hints based on that information. This lead to a lot of complexity and broke some editor integration, while offering little benefit. It now focuses on making sure best-practices are being followed.

ESLint itself had a major release which broke this plugin originally, and Meteor 1.3’s package support was announced which would have made the original version of this plugin even more complicated. Development of this plugin was paused until things settled down. Now that they have, ESLint-plugin-Meteor has been adapted to the changes and it’s more useful than ever :-)