@glorification/eslint-config · @glorification/prettier-config

Stop arguing about style in code review

Glorification is a modern coding style for modern JavaScript and TypeScript, packed as an ESLint config and a Prettier config. You install it once. From then on, every file you save looks like it was written by the same careful person.

npm versionESLintlicense

Here is a small user service, written the way most JavaScript is written today: Prettier's defaults and the habits everybody picked up along the way. Next to it is the same service in the modern coding style.

✗ Today's typical JavaScript
import fs from "fs";

const DEFAULT_TIMEOUT = 30000;

export default class UserService {
  constructor(baseUrl, options) {
    this.baseUrl = baseUrl;
    this.options = options || {};
  }

  get(id) {
    if (this.options.cache === true && cache.users[id]) {
      return Promise.resolve(cache.users[id]);
    }
    const url = this.baseUrl + "/users/" + id;
    return fetch(url, { timeout: DEFAULT_TIMEOUT })
      .then((response) => response.json())
      .then((user) => {
        cache.users[id] = user;
        return user;
      });
  }
}
✓ The modern coding style
const DEFAULT_TIMEOUT = 30_000

class UserService {
    constructor(baseUrl, options = {}) {
        this.baseUrl = baseUrl
        this.options = options
    }

    async get(id) {
        if (this.options.cache && cache.users[id]) return cache.users[id]
        const response = await fetch(`${this.baseUrl}/users/${id}`, { timeout: DEFAULT_TIMEOUT })
        const user = await response.json()
        cache.users[id] = user
        return user
    }
}

export default UserService

Same behavior, less noise. No semicolons, no .then ladder, no === true, nooptions || {}. Every line on the right passes the config, and every line on the left is reported by it. The rest of this post walks through what changed and why.

A style in your head drifts. A style in a config doesn't.

Every team has a style. Most of the time it lives in people's heads, in a wiki page nobody reads, and in review comments like "nit: we use arrow functions here". People forget, new people never knew, and every review spends some of its time on how the code looks instead of what it does.

A style that lives in a config is applied on every save, in every file, by every contributor. Nobody has to remember it, and nobody has to be the person who points it out. That is the whole idea of Glorification: take a modern coding style and make it something a machine checks.

What modern looks like

1. No semicolons, except the one place you need one

JavaScript inserts semicolons for you. In practice there is exactly one trap: a line that starts with(, [ or a backtick joins the line above it. The modern coding style drops every semicolon and puts that rare one at the start of the risky line, where you can see it.

✗ Before
const total = items.length;
const names = items.map((item) => item.name);

[first, second] = [second, first];
(async () => {
  await save(names, total);
})();
✓ The modern coding style
const total = items.length
const names = items.map(item => item.name)

;[first, second] = [second, first]
;(async () => {
    await save(names, total)
})()

Prettier (semi: false) and ESLint agree on this rule exactly, so the two tools never undo each other's work.

2. Arrow functions, and short ones where possible

Top-level functions are arrow constants. An arrow that only returns a value uses the short form, and one plain parameter needs no parentheses. A function keyword stays only where you really needthis or a generator.

✗ Before
function double(value) {
  return value * 2;
}

function sum(values) {
  return values.reduce(function (total, value) {
    return total + value;
  }, 0);
}
✓ The modern coding style
const double = value => value * 2

const sum = values => values.reduce((total, value) => total + value, 0)

3. async / await instead of a .then ladder

A promise chain reads inside out. await reads top to bottom, like the rest of your code, and a normal try / catch handles the errors.

✗ Before
function loadUser(id) {
  return fetch("/users/" + id)
    .then((response) => response.json())
    .then((user) => {
      return { ...user, loadedAt: Date.now() };
    })
    .catch((error) => {
      report(error);
      throw error;
    });
}
✓ The modern coding style
const loadUser = async id => {
    try {
        const response = await fetch(`/users/${id}`)
        const user = await response.json()
        return { ...user, loadedAt: Date.now() }
    } catch (error) {
        report(error)
        throw error
    }
}

4. Check flags as themselves

if (flag === true) says the same thing twice. The config reports it, and a one-lineif needs no braces.

✗ Before
if (options.verbose === true) {
  log(message);
}
if (user.isAdmin === true) {
  grantAll(user);
}
✓ The modern coding style
if (options.verbose) log(message)
if (user.isAdmin) grantAll(user)

5. Real defaults

= undefined is not a default, options = options || {} changes a parameter, and an optional parameter before a required one can never be left out. Give the real default in the signature instead.

✗ Before
function connect(host, port = undefined, options) {
  options = options || {};
  return open(host, port || 8080, options);
}
✓ The modern coding style
const connect = (host, options = {}, port = 8080) => open(host, port, options)

6. Trailing commas, for diffs that tell the truth

Every multi-line object, array, argument list and parameter list ends with a comma. It looks odd for a week. Then you notice your diffs: adding an item touches one line, not two.

✗ Adding "viewer" without trailing commas
 const roles = [
   "admin",
-  "editor"
+  "editor",
+  "viewer"
 ];
✓ With trailing commas
 const roles = [
     'admin',
     'editor',
+    'viewer',
 ]

7. Numbers you can read

Quick: is 10485760 ten million or a hundred million? Numbers of five digits or more use_ separators. Hex, binary and octal stay as you wrote them.

✗ Before
const MAX_UPLOAD = 10485760;
const TIMEOUT = 30000;
const RETRY_DELAY = 1000;
const SEED = 0x2f6e2b1;
✓ The modern coding style
const MAX_UPLOAD = 10_485_760
const TIMEOUT = 30_000
const RETRY_DELAY = 1000
const SEED = 0x2f6e2b1

8. No else after return

Once a branch returns, the code after the if already is the else. Early returns keep the happy path flat and to the left.

✗ Before
function priceLabel(price) {
  if (price === 0) {
    return "free";
  } else if (price < 10) {
    return "cheap";
  } else {
    return "$" + price;
  }
}
✓ The modern coding style
const priceLabel = price => {
    if (price === 0) return 'free'
    if (price < 10) return 'cheap'
    return `$${price}`
}

Standing on the shoulders of giants

Glorification doesn't start from zero. It merges the three most used rule sets, Airbnb,StandardJS and eslint:recommended, removes the duplicates, and keeps every bug catcher they agree on: no-undef, eqeqeq (with == nullallowed), no-var, no-throw-literal, the Node and Promise checks, and more.

It also keeps a few choices that keep code honest: no shadowed names, parameters are read-only, everyswitch has a default, and a function returns a value on every path or on none.

And it allows what a modern style simply allows: i++, bitwise operators,a = b = c(), object spread, _private helpers, nested ternaries, for…in,(a, b) sequences and () => (count = 0). A rule that only makes you type more is not in the config.

Go all the way: the optional extras

The base config is the style everyone can live with. The extras go further. Each one is off by default, and you add only the ones you want.

Sorting

A sorted file reads like a table: you find a key, a field or an import without scanning. Object keys, destructuring, class fields and named imports are A to Z, and the constructor is always the first member of a class.

✗ Before
import { writeFile, mkdir } from "node:fs/promises";

const user = { name: "Ada", id: 7, email: "ada@example.com" };
✓ With sorting
import {
    mkdir,
    writeFile,
} from 'node:fs/promises'

const user = { email: 'ada@example.com', id: 7, name: 'Ada' }

Aliases

../../ paths break the moment you move a file. With Node's subpath imports every folder gets a stable name, and builtins carry the node: prefix.

✗ Before
import path from "path";
import { toJs } from "../../io/utils.js";
import { writeTextFile } from "../../utils/fileUtils.js";
✓ With aliases
import path from 'node:path'
import { toJs } from '#io/utils.js'
import { writeTextFile } from '#utils/fileUtils.js'

JSDoc

Every function, class and method gets a JSDoc block, written the modern way: the description is a sentence, shapes are named @typedefs, arrays are T[], and a bare Function is never enough.

✗ Before
/**
 * computes the area
 * @param {{ width: number, height: number }} rect
 *
 * @param {Function} round
 */
const area = (rect, round) => round(rect.width * rect.height);
✓ With the JSDoc extra
/**
 * Area of `rect`, rounded.
 *
 * @param {Rect} rect
 * @param {(value: number) => number} round
 * @returns {number}
 */
const area = (rect, round) => round(rect.width * rect.height)
// eslint.config.js: the modern coding style with every extra
import glorification from '@glorification/eslint-config'
import aliases from '@glorification/eslint-config/aliases'
import jsdoc from '@glorification/eslint-config/jsdoc'
import sorting from '@glorification/eslint-config/sorting'

export default [...glorification, ...aliases, ...jsdoc, ...sorting]

And a few habits no tool can check

The last step is done by hand, in code review: parameters in order (required A to Z, then optional A to Z), long lines broken after && and ||, constants in UPPER_CASE, every object@typedef with its own factory, one-line comments that say what or why (never the history), and end-of-line comments that line up:

✓ Hand-applied
const TIMEOUT = 30_000    // milliseconds per request
const RETRIES = 3         // attempts before giving up
const BACKOFF = 1.5       // delay multiplier between attempts

Make it yours

The modern coding style is a starting point, not a cage. Don't like a rule? Turn it off. Miss one? Add it. Youreslint.config.js is a normal flat config: put your changes after the base config, and for each rule the last setting wins.

// eslint.config.js
import glorification from '@glorification/eslint-config'

export default [
    ...glorification,
    {
        rules: {
            'no-console': 'off', // a rule you find annoying
            'no-nested-ternary': 'error', // a rule you miss
        },
    },
    {
        files: ['**/*.test.js'],
        rules: {
            'no-param-reassign': 'off', // only for some files
        },
    },
]

Some good rules are off on purpose, like no-sequences, no-nested-ternary andguard-for-in. The README lists them underWant it stricter?, with what each one catches.

ESLint and Prettier that never fight

The usual setup has a quiet war in it: Prettier formats, ESLint complains, ESLint fixes, Prettier formats again. Glorification's two packages were built together and agree on every rule they share: quotes, semicolons, commas, indentation and line width. You can run them in any order and get the same file. On save in WebStorm, ESLint runs first and Prettier second, and nothing flips back and forth.

Two minutes to set up

npm i -D eslint prettier @glorification/eslint-config @glorification/prettier-config
// eslint.config.js
import glorification from '@glorification/eslint-config'

export default glorification
// package.json
{
    "prettier": "@glorification/prettier-config"
}

WebStorm: turn on Run on save for Prettier and Run eslint --fix on save for ESLint, and turn off Reformat code in Actions on Save. VS Code:

// .vscode/settings.json
{
    "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" },
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true
}

TypeScript is one more line: add @glorification/eslint-config/ts after the base config.

Try it

Install it on a project you know well, run npm run lint -- --fix, and read the diff. Most of it fixes itself. What's left is usually worth a look.