Jacob Paris
← Back to all content

Create a custom local eslint rule

We have a styleguide at work that says all of our labels should be in sentence-case, so instead of "Add Products" it's "Add products" with a lowercase 'p', and instead of "User Account" it's "User account" with a lowercase 'a'.

It's easy enough to follow when you're paying attention, and even when you're not it'll often be caught in code review, and even when it's not it's a fairly minor defect in the final product that will probably get fixed later. But the time spent fixing it later, plus the time spent finding it later, plus the time spent looking for it in code reviews, all adds up to more time than needs to be spent.

And besides: reviewers with a preoccupation for being human linters are spending effort that could be better spent on their regular occupation of being human reviewers.

As it turns out, running custom ESLint rules locally is really easy, so there's lots of room to explore automating rules and guidelines that are important to your project but maybe not anyone else. I used the package eslint-plugin-local-rules which makes this a breeze.

bash
npm install eslint-plugin-local-rules --save-dev

Local rules go in a local directory, so I made a new file at ./eslint-local-rules/no-title-case.js and pieced together enough example code and stack overflow responses until I had a plugin that looked right

js
1function checkString(str, node, context) {
2 // \p{Ll} matches a lowercase letter that has an uppercase variant
3 // \s matches any whitespace character
4 // \p{Lu} matches an uppercase letter that has a lowercase variant
5
6 /**
7 * Look for the pattern {lowercase} {space} {uppercase} {lowercase}
8 */
9 const lowerToUpperCount = (
10 str.match(/\p{Ll}\s\p{Lu}\p{Ll}/gu) || []
11 ).length
12
13 if (lowerToUpperCount > 0) {
14 context.report({
15 node,
16 message:
17 'String literals should not use title case. For titles, where that may be wanted, use the class ".text-capitalize"',
18 fix(fixer) {
19 return fixer.replaceText(
20 node,
21 node.raw.replace(
22 /\s\p{Lu}\p{Ll}/gu,
23 (token) => token.toLowerCase(),
24 ),
25 )
26 },
27 })
28 }
29}
30
31module.exports = {
32 meta: {
33 docs: {
34 description:
35 "Enforce that string literals aren't in title case.",
36 recommended: false,
37 },
38 fixable: "code",
39 },
40
41 create: function (context) {
42 return {
43 Literal: (node) => {
44 if (typeof node.value === "string") {
45 checkString(node.value, node, context)
46 }
47 },
48 JSXText: (node) => {
49 checkString(node.value, node, context)
50 },
51 }
52 },
53}
54

The eslint-local-rules plugin actually looks for an index file containing all the rules, so I started a collection there at ./eslint-local-rules/index.js

js
1module.exports = {
2 "no-title-case": require("./no-title-case"),
3}
4

And the last step was to add the plugin and enable the rule in the .eslintrc file. I didn't want to be too aggressive here and block CI from passing, so I set the rule to warn instead of throw errors.

js
1module.exports = {
2 plugins: ["local-rules"],
3 rules: {
4 "local-rules/no-title-case": "warn",
5 },
6}
7
Professional headshot

Hi, I'm Jacob

Hey there! I'm a developer, designer, and digital nomad with a background in lean manufacturing.

About once per month, I send an email with new guides, new blog posts, and sneak peeks of what's coming next.

Everyone who subscribes gets access to the source code for this website and every example project for all my tutorials.

Stay up to date with everything I'm working on by entering your email below.

Unsubscribe at any time.