Improving DX in Ember, Part 3: Ember Suave 2.0

By: Sergio Arbeo
Person hanggliding over mountains

This series has focused on improving the Ember developer experience. While the quick wins and best practices were a great start, we’d like to introduce Ember Suave 2.0, a new version of rules to enhance the DX even more in Ember projects and beyond. To ease your update path, this blog post will walk you through eslint-plugin-ember-suave 2.0, most of the new rules, why we included them, and how to fix them if they cannot be autofixed.

New Rules

Ember Related

Only one rule is specific to Ember, ember/new-modules-import. We were pretty outdated in this part, so we just rely on eslint-plugin-ember to provide this functionality. We are removing require-const-for-ember-properties as we feel that the new rule supersedes this one.

Async Functions

A few new rules are related to async functions. This will help your team not to step on known mistakes when using asynchronous code.

The first one we included is no-await-in-loop. While there might be a case where using await inside a loop makes sense, it is a really rare scenario. Usually, Promise.all is the way you want to go as it will make all the promises run asynchronously.

// Bad
const fetchAllResources = async (resources) => {
  const result = [];

  for (let i = 0; i < resources.length; i++) {
    result.push(await fetch(resources[i]));
  }

  return result;
}

// Good
const fetchAllResources = async (resources) => {
  return await Promise.all(resources.map(resource => fetch(resource)));
}

The code is much sorter, readable, and it fetches all the resources asynchronously. Meaning you don’t need to wait for the first resource to download to start fetching the second one.

But the Good version contains a new error: why are we doing return await? That would be the same as doing myPromise.then(x => x).then(actuallyDoingSomething). We have included another rule to prevent that: no-return-await.

// Bad
const fetchAllResources = async (resources) => {
  return await Promise.all(resources.map(resource => fetch(resource)));
}

// Good
const fetchAllResources = (resources) => {
  return Promise.all(resources.map(resource => fetch(resource)));
}

Note that the async is gone as well; we don’t need it anymore as we are not awaiting anything.

The last rule we added, no-async-promise-executor, prevents you from using an async function as the parameter to the Promise constructor. You don’t need to create a new promise if you already have an async function.

// Bad
const getCurrentStatus = () => {
  return new Promise(async (resolve) => {
    let response = await fetch(STATUS_URL);
    resolve(await response.text());
  });
}

// Good
const getCurrentStatus = async () => {
  let response = await fetch(STATUS_URL);
  return response.text();
}

Import Statements

Another area we are adding some new lint rules for are import statements.

The first rule we included is no-duplicate-imports. This one is great to keep all the imports from the same module in only one statement. Depending on your environment, you might get code auto-imported in its own line, finding the following scenario in your code:

// Bad
import { a } from './a';
import { b } from  './b';
import { c } from './a';

This rule forces the code to only use one import per file:

// Good
import { a, c } from './a';
import { b } from  './b';

The next rule might seem off at first, but after working with it for a few months, we found the code easier to read, especially in Ember projects, and better organized. This rule is import/no-relative-parent-import, and it prevents your code from importing from a parent directory using relative paths. In an Ember project, it prevents the programmer from doing a statement like import myUtil from '../utils/my-util'; favoring import myUtil from 'my-app/utils/my-util'; instead.

There are several reasons for this:

  1. In a nested component, the situations is worse: a path like '../../../utils/some-util' is not simple to parse.
  2. If you need to import something from a nested test, it is even worse, as the relative path for the module does not even ressemble the relative path for the file.
  3. Slowly, you have a nicer and more robust code organization.
  4. The consuming module won’t need to update the import path if moved.

Standard ESLint

Finally, we also included some standard ESLint rules.

  1. eqeqeq is added to our recommended config. This forces the user to use === instead of ==;
  2. We added a couple of new rules to force some padding lines between object or class properties.
  3. We added padding-line-between-statements to force an empty line after blocks.

Changes to Old Rules

But what happened to the old rules? We removed require-const-for-ember-properties, but almost all the others stayed as they were. We only changed prefer-destructuring to not force you to destructure arrays.

Destructuring arrays are not exactly the same as accessing a property, and when the index is over 1, it gets pretty unreadable (let [,,myVar] = someCollection;).

In any case, if you still want the old behaviour, just override the rule in your local config.

What’s Next in eslint-plugin-ember-suave?

First, and this is a personal quest, is improving the plugin’s testing. We have pretty good fixtures but, currently, our system does not check that the failing rule is the one we intend to test. So, while we can guarantee that the fixtures that are supposed to be valid are ok, we cannot say that the failing ones are failing the way we intend to.

Second, we are renaming the project too. Soon, eslint-plugin-ember-suave will be deprecated and become eslint-plugin-dockyard. We have had an open issue about this for some time, and we feel our configuration can be useful for non-Ember projects as well. If you update to ember-suave 2.0, it’ll be simple to move to eslint-plugin-dockyard once we get it out.

Third and last, but not least, we will explore some ways to include new breaking rules without shipping any breaking change. We want a way not to ship so many new rules at once without forcing you, the user, to fix your code with every minor release. This is for the future though.

Series Recap

You made it to the last post in the series. Thank you!

In the first post, Quick Wins, we discuss tips to get some quick wins in DX for your Ember project.

In the second post, Changing Our Toolbelt, we went through some major changes to our project that brought bigger improvements.

In this closing post, we had a great announcement, with the newer and better eslint-plugin-ember-suave.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js and Elixir. With a nationwide staff, we’ve got consultants in key markets across the U.S., including Seattle, Portland, San Francisco, Denver, Chicago, Dallas, Atlanta, and New York.