DEV Community

Isaac Lee
Isaac Lee

Posted on • Originally published at crunchingnumbers.live

2

It’s Time to Separate: Lint and Format

In the It's Time to Separate series, I'll show you how separating concerns helps us simplify code. And that separation can occur in many places.

Little advertised news from January: Ember CLI removed eslint-plugin-prettier and stylelint-prettier from its blueprints, adding instead two scripts to run prettier. I was skeptical when the RFC had come out. Why change things when these plugins have worked well for years? Isn't it, for people who maintain many projects like me, huge effort, little reward?

Then I realized last week, separating formatting from linting is indeed a good thing. Not just cause of the performance gain that's usually cited. But because it helps us reexamine past approaches and come up with simpler ones that will stand the test of time.

1. How to migrate

Separation was painless. You could run ember-cli-update (set the version to 6.3.0 or higher). But likely you can't cause you have Ember v5 or less, and didn't address all the deprecations.

You don't need to. Separate the Prettier migration from the Ember update. Here are the steps, assuming that your project didn't deviate much from the blueprints. If you get lost, you can look at my open-sourced configs.

1. In package.json, remove eslint-plugin-prettier and stylelint-prettier. Update the scripts to separate formatting and linting.

/* package.json */
{
  "scripts": {
+     "format": "prettier . --cache --write",
    "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
    "lint:css": "stylelint \"**/*.css\" --cache",
    "lint:css:fix": "pnpm lint:css --fix",
-     "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"",
+     "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" && pnpm format",
+     "lint:format": "prettier . --cache --check",
    "lint:js": "eslint . --cache",
    "lint:js:fix": "pnpm lint:js --fix"
  },
  "devDependencies": {
    "concurrently": "...",
    "eslint": "...",
    "eslint-config-prettier": "...",
-     "eslint-plugin-prettier": "...",
    "prettier": "...",
    "prettier-plugin-ember-template-tag": "..."
    "stylelint": "...",
-     "stylelint-prettier": "..."
  }
}
Enter fullscreen mode Exit fullscreen mode

If you have shared configurations for eslint and stylelint, you can remove prettier from peer dependencies.

/* package.json (eslint config) */
{
  "peerDependencies": {
    "eslint": "^9.0.0",
-     "prettier": "^3.0.0",
    "typescript": "^5.0.0"
  },
  "peerDependenciesMeta": {
    "typescript": {
      "optional": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Remove eslint-plugin-prettier from the eslint configuration.

/* eslint.config.mjs (eslint@v9) */
import eslint from '@eslint/js';
- import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
+ import eslintConfigPrettier from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  eslint.configs.recommended,
-   eslintPluginPrettier,
+   eslintConfigPrettier,
);
Enter fullscreen mode Exit fullscreen mode
/* .eslintrc.cjs (eslint@v8) */
'use strict';

module.exports = {
  extends: [
    'eslint:recommended',
-     'plugin:prettier/recommended',
+     'prettier',
  ],
};
Enter fullscreen mode Exit fullscreen mode

Note, we added eslint-config-prettier to turn off eslint rules that may conflict with prettier. eslint-plugin-prettier used to do this for us.

Also worth knowing, eslint-plugin-prettier had to be the last plugin in order to override rules from the preceding ones. You'll be fine if you list eslint-config-prettier where eslint-plugin-prettier used to be.

3. Remove stylelint-prettier from the stylelint configuration.

/* .stylelintrc.mjs */
export default {
-   extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
+   extends: ['stylelint-config-standard'],
};
Enter fullscreen mode Exit fullscreen mode

4. Run install to update your dependencies. Run lint:css and lint:js to check that files can be linted.

5. Prettier now checks more files than it used to via linter plugins. Run lint:format to identify files that shouldn't be formatted. List them in .prettierignore. Here is the starter config for Ember apps:

# unconventional js
/blueprints/*/files/

# compiled output
/dist/

# misc
/coverage/
!.*
.*/
*.html
pnpm-lock.yaml

# specific to my package
...
Enter fullscreen mode Exit fullscreen mode

Note, *.html was added as a temporary fix for a bug in Prettier. pnpm-lock.yaml isn't needed if the package is a workspace in a monorepo.

Migration complete! What's nice about the names format and lint:format is backward compatibility. Just like before, developers need to know only 3 scripts to review their work:

# Check errors
pnpm lint

# Fix errors
pnpm lint:fix

# Run tests
pnpm test
Enter fullscreen mode Exit fullscreen mode

2. How to format hbs

Ember aficionados may notice a gap. What about Glimmer templates?

Until now, we had to use ember-template-lint-plugin-prettier to format *.hbs files. This is a bit strange, because Prettier natively supports Handlebars since May 2021.

The plugin also comes with a few issues:

  • It uglifies code inside an hbs tag (i.e. wrong indentations in rendering tests, Storybook stories).
  • It needs to dynamically load prettier and use a hook from ember-template-lint to format *.hbs. Due to strong coupling, it will fall behind if prettier or ember-template-lint makes a breaking change to their API.
  • Prettier recommends not running prettier through a linter plugin. Partly why Ember CLI removed the eslint and stylelint plugins.

Thanks to the separation, we can see that all we need now is a Prettier plugin that formats hbs tags.

Enter prettier-plugin-ember-hbs-tag, a plugin that I wrote last week. Just like prettier-plugin-ember-template-tag, it has one job and doesn't need a linter to run. Here is the starter config:

/* prettier.config.mjs */
export default {
  plugins: [
    'prettier-plugin-ember-hbs-tag',
    'prettier-plugin-ember-template-tag',
  ],
  overrides: [
    {
      files: ['*.{cjs,cts,js,mjs,mts,ts}'],
      options: {
        singleQuote: true,
      },
    },
    {
      files: ['tests/**/*-test.{js,ts}'],
      options: {
        parser: 'ember-hbs-tag',
        singleQuote: true,
        templateSingleQuote: false,
      },
    },
    {
      files: ['*.{gjs,gts}'],
      options: {
        singleQuote: true,
        templateSingleQuote: false,
      },
    },
    {
      files: ['*.hbs'],
      options: {
        printWidth: 64,
        singleQuote: false,
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Then, remove ember-template-lint-plugin-prettier from the ember-template-lint config and uninstall the package:

/* .template-lintrc.cjs */
'use strict';

module.exports = {
-   plugins: ['ember-template-lint-plugin-prettier'],
-   extends: ['recommended', 'ember-template-lint-plugin-prettier:recommended'],
-   overrides: [
-     {
-       files: ['**/*.{gjs,gts}'],
-       rules: {
-         prettier: 'off',
-       },
-     },
-     {
-       files: ['tests/**/*-test.{js,ts}'],
-       rules: {
-         prettier: 'off',
-       },
-     },
-   ],
+   extends: ['recommended'],
};
Enter fullscreen mode Exit fullscreen mode
/* package.json */
{
  "devDependencies": {
    "ember-template-lint": "...",
-     "ember-template-lint-plugin-prettier": "...",
    "prettier": "...",
+     "prettier-plugin-ember-hbs-tag": "...",
    "prettier-plugin-ember-template-tag": "..."
  }
}
Enter fullscreen mode Exit fullscreen mode

With that, you can format everything. Don't forget to star. ✨

3. Even more separation

Starting with ember-template-lint@7.7.0, you can sort invocations.

/* .template-lintrc.cjs */
'use strict';

module.exports = {
  extends: ['recommended'],
  rules: {
    'sort-invocations': true,
  },
};
Enter fullscreen mode Exit fullscreen mode

In order to fix a long-standing issue, sort-invocations leaves formatting up to the two Prettier plugins. Stay tuned for the dedicated post.

Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

pretty cool seeing all these steps laid out, honestly i get stuck in config hell way too often - you think most projects lose speed just from not trimming old stuff or is it more the people side holding things up?

Collapse
 
ijlee2 profile image
Isaac Lee

Hi, there. Appreciate your feedback and question.

I do think people can underestimate how much easier extending code would be, if maintenance is done on a regular basis, in small increments. And that we should treat maintaining code (e.g. refactoring, updating dependencies, removing unused deps.) like a handcraft—a valuable skill that people can learn and refine.

As for your second question: Based on chats with people at Ember conferences and in the Ember Discord, company size and work culture could be factors, sure. I'm in a fortunate situation to work somewhere that has dedicated platform teams. My understanding is, the concept of having such a platform team is relatively new, so it may take some time until we see more companies follow the footsteps because they believe that maintenance is crucial in development.

👋 Kindness is contagious

Appreciate this enlightening write-up and be part of the vibrant DEV Community. Developers of every background are invited to pitch in and enrich our collective expertise.

A simple “thanks” can make someone’s day. Share your gratitude in the comments.

On DEV, knowledge-sharing lights our journey and strengthens our ties. Enjoyed the article? A kind note to the author goes a long way.

Join the community