I’ve worked on quite a few JavaScript and Vue projects over the years, and one thing that started to annoy me was maintaining the same linting configuration across all of them.

Every project had its own ESLint and Stylelint configuration. Most of the rules were identical, but whenever I wanted to change something, I had to update each repository individually.

It also meant that newer projects would sometimes have different rules from older ones, depending on when I’d last copied the configuration across.

I decided to move the configurations into two separate packages, so that I could share the same coding standards across all my projects.

Why bother with linting?

I’ve always liked keeping code consistent. Not necessarily because one particular way of writing JavaScript or CSS is better than another, but because it’s easier to read when everything follows the same conventions.

I don’t particularly care whether somebody prefers single or double quotes, or whether they like semicolons. What I don’t want is a project where half the files use one convention and the other half use something different.

ESLint handles JavaScript, TypeScript and Vue, while Stylelint handles CSS and SCSS.

Both have plenty of existing configurations, but I wanted something that matched how I prefer to write code.

Rather than repeatedly configuring them for every project, I created two reusable packages.

Creating a shared ESLint configuration

The ESLint configuration is built on top of Nuxt’s existing ESLint configuration, with a few additional rules.

The current version uses ESLint’s flat configuration format, which means the configuration is exported as a JavaScript module.

1import { createConfigForNuxt } from '@nuxt/eslint-config'
2 
3export default await createConfigForNuxt()
4 .override('nuxt/javascript', {
5 rules: {
6 'no-console': 'error',
7 'camelcase': ['error', {
8 properties: 'always'
9 }],
10 'prefer-template': 'error',
11 'quotes': ['error', 'single'],
12 'semi': ['error', 'never']
13 }
14 })
Copied!

Some of these are just personal preferences.

I prefer single quotes, don’t use semicolons, and use template literals instead of concatenating strings.

Others are useful for preventing mistakes, such as disallowing unexpected console.log() statements.

I also added rules for Vue components, including enforcing PascalCase component names in templates, kebab-case custom events, and a consistent ordering of the component’s sections.

For example, I prefer Vue single-file components to have their styles first, followed by the template and then the script.

1<style scoped>
2.example {
3 color: red
4}
5</style>
6 
7<template>
8 <div class="example">
9 Hello World
10 </div>
11</template>
12 
13<script setup>
14</script>
Copied!

There’s no technical advantage to that particular order. It’s just the way I’ve become accustomed to organising Vue components.

But by adding the rule to ESLint, I don’t need to think about it anymore.

Adding TypeScript rules

As I’ve moved more of my projects to TypeScript, I also wanted to enforce some naming conventions.

I added rules using @typescript-eslint to ensure types and interfaces use PascalCase, while their properties use camelCase.

1'@typescript-eslint/naming-convention': [
2 'error',
3 {
4 selector: 'typeLike',
5 format: ['PascalCase']
6 },
7 {
8 selector: 'typeProperty',
9 format: ['camelCase']
10 }
11]
Copied!

This prevents inconsistencies from appearing as a project grows.

I’ve also added a rule requiring a blank line before return statements.

It’s a small formatting preference, but I think it makes functions easier to scan, particularly when there’s a bit of logic before the return.

The nice thing about having these rules in a shared package is that I can introduce a new convention once and make it available to every project using the configuration.

Doing the same for Stylelint

The Stylelint configuration follows the same idea, but CSS has a slightly different problem.

As well as keeping formatting consistent, I wanted CSS properties to appear in a predictable order.

There are plenty of opinions about how CSS properties should be ordered. Some people prefer alphabetical ordering, while others group related properties together.

I prefer grouping properties by what they do.

For example, positioning properties should appear together, followed by layout, typography and visual styling.

Rather than relying on memory, I wanted Stylelint to handle it.

Creating a custom CSS property ordering rule

I ended up creating a custom Stylelint plugin called plugin/rational-order.

It builds on stylelint-order, but defines my preferred groups of properties.

The configuration separates properties into categories including positioning, box model, typography, visual styling and animation.

Here’s a simplified version of the configuration:

1module.exports = () => [
2 ['Positioning', positioning],
3 ['Box Model', boxModel],
4 ['Typography', typography],
5 ['Visual', visual],
6 ['Animation', animation],
7 ['Misc', misc]
8].map(([groupName, properties]) => ({
9 emptyLineBefore: 'never',
10 properties,
11 groupName
12}))
Copied!

The actual implementation also has options for whether border properties belong with the box model or visual styling, and whether groups should be separated by blank lines.

This means CSS can be written in a consistent, logical order without having to remember where every property belongs.

For example:

1.card {
2 position: relative;
3 display: flex;
4 align-items: center;
5 width: 100%;
6 margin: 1rem 0;
7 padding: 1rem;
8 color: black;
9 background-color: white;
10 border-radius: .5rem;
11 opacity: 1;
12 transition: opacity 200ms ease;
13}
Copied!

The properties follow a predictable structure, making it easier to find what I’m looking for when returning to a component.

The custom plugin generates the property order and passes it to Stylelint’s existing ordering rule, so I didn’t need to write all of the checking and automatic fixing logic myself.

Keeping the rest of the CSS consistent

The shared Stylelint configuration extends the standard SCSS and recommended Vue configurations, with additional rules for how I prefer to write CSS.

For example, I enforce uppercase hexadecimal colours, lowercase property names, no unnecessary units on zero values, and consistent spacing around declarations.

I also disallow !important and vendor-prefixed properties where they’re unnecessary.

A lot of these are minor things, but they add up.

I don’t want to spend time reviewing whether somebody used a shorthand property correctly or put an unnecessary unit on 0.

That’s something a linter can check automatically.

Installing the configurations

Both packages are published through GitHub Packages, so I can install them into a project using a package manager.

Because they’re distributed through GitHub Packages, the project also needs an .npmrc configured to use the correct package registry, with appropriate authentication.

Once installed, ESLint can use the shared configuration directly.

For Stylelint, the project can extend the shared configuration rather than maintaining its own list of rules.

The result is that new projects only need a small amount of configuration to follow the same standards as the existing ones.

Running the linters

I normally add scripts to package.json so that linting can be run consistently across projects.

1{
2 "scripts": {
3 "lint:scripts": "eslint .",
4 "fix:scripts": "eslint . --fix",
5 "lint:styles": "stylelint \"**/*.{vue,scss}\"",
6 "fix:styles": "stylelint \"**/*.{vue,scss}\" --fix"
7 }
8}
Copied!

I also use the ESLint and Stylelint extensions in VS Code, with automatic fixes enabled when saving a file.

That means most formatting issues are corrected while I’m writing code, rather than being left until somebody runs the linter manually.

For the issues that can’t be fixed automatically, the editor shows the problem immediately.

Maintaining the packages

There is one downside to having shared configurations: changing a rule can suddenly introduce errors across multiple projects.

That’s particularly noticeable when upgrading major versions of ESLint or Stylelint, as some rules get renamed, removed or moved into different plugins.

I’ve had to update both configurations over time to keep up with changes to the tooling.

Using versioned packages helps with this. Projects can update when they’re ready, rather than every application suddenly changing behaviour because I modified a shared file.

I also try to keep the configurations reasonably opinionated without making them unnecessarily restrictive.

There are always going to be exceptions, and I’d rather disable a rule for a particular case than make the code worse just to satisfy the linter.

Was it worth it?

For a single small project, maintaining your own ESLint and Stylelint packages would probably be overkill.

But when you’re working across several repositories, having the same coding standards everywhere is useful.

I no longer need to copy configuration files between projects or remember which repository has the most recent rules.

New projects can start with the conventions I already use, and existing projects can pick up improvements by updating their dependencies.

It also means I spend less time worrying about formatting when reviewing code.

The linter handles the small, repetitive things, leaving me to focus on whether the code actually works and whether it’s a sensible way of solving the problem.