Success StoriesBlogContact
piece of paper with two pencils and some handwritten lines of code
Webentwicklung |

Scripts need love too!

Moritz Jacobs

4. März 2020

tl;dr quick summary
Why npm scripts deserve the same attention to quality and maintainability as every other aspect of our code and introducing scriptlint.

One of the most unique and appealing aspects of modern JavaScript development is the tooling that revolves around it. Remember the times when your teammates had strong opinions on indentation and scrutinized each other for checking in “bad” or “ugly” code? Nowadays, ESLint and Prettier take care of that problem by cementing those opinions in configuration and automatically scrutinizing you before you can even compile that code. Testing is another example: we use Jest for unit tests, Cypress for e2e tests and all kinds of pretests, checks and other linters. And we run all of that locally as well as in the CI environment. All that tooling is hard to set up and has a steep learning curve, but once it works, it does a great job of maintaining code quality and maintainability, even in bigger teams.

The typical entry point to these tools is package.json ’s "scripts" section, also known as “npm scripts” (yet they can be executed with yarn and other npm alternatives as well). They provide easy CLI access to all the tooling in your project. But when it comes to maintanability of those scripts, they’re somewhat neglected. If you ever worked in a big and mature JS codebase, you’ve seen it: dozens and dozens of build, lint and test scripts, sometimes hooking/calling each other using pre*or post* prefixes, running stuff sequentially using && and almost always in a seemingly random order that makes it hard to find what you’re looking for. What a nightmare.

Even worse, every scripts section uses different — if any — naming and calling conventions. Maybe npm run start triggers prestart, which runs rm -rf ./dist before Babel transpiles it all and poststart starts a server? Maybenpm run start starts the local dev server plus some kind of watcher? Maybe you have to start that via start:development (or was it start-develop?) — without looking at package.json directly — who knows? You can’t add comments in package.json and there’s hardly ever any README section for the scripts… oh and what if you onboard a new team member who insists on using Windows? rm -rf ./dist will fail on Windows.

test:lint or test-lint, which was it again?

Can we PLEASE find some way to organize this in a consistent way!? Can’t we agree on some best practices!? I hear you scream. Well, your friends at Peerigon have just the right thing to ease your pain.

State of package.json 2020

This is somewhat of a blog post topic in itself, but we took an in-depth look at how people use package scripts. We crawled the package.jsons of the 1000 most depended upon npm modules (there are better metrics out there, if you have the data – this was just easily crawlable) and looked at their "scripts". A few common patterns emerged:

1. People namespace their scripts

You see a lot of build:* or test:* names, which provide two advantages: it groups scripts together both visually and alphabetically (some people maintain alphabetic order using their editors sort-line functionality).

2. : is a very common namespace seperator

It’s unclear who started it, but it seems to be the favorite. Another popular option is - .

3. People divide and conquer

To make processes easier to maintain, it is very common to divide a complex script up into smaller parts and then run them sequentially. Mostly this is done by using && — which works on *nix and Windows systems. Another best practice you see is using npm-run-all. Using custom hooks (pre*/post*) is less common but also a valid (yet arguably unintuitive) approach.

4. Some people just don’t care

There are all kinds of best practices reflected in these findings, but also a lot of projects could use some help with their scripts.

Don’t call it a “standard”

screenshot of a tweet from tim myers about script standards

Tim Myers on Twitter

As tech people we tend to reach for a “standard” every time we aren’t sure how to do something “right”. There’s this famous xkcd comic about standards that is a testament to software developers’ tendency to invent new standards if the old ones don’t fit their needs.

In this case there was no standard. Naming and designing npm scripts is uncharted territory and as a newcomer, you’re left to your own devices. Senior JS developers have a way of doing things “right” by experience, but no way of explaining why or how.

Still, we wanted to abstain from writing a complicated and abstract technical standard paper that no one is able to read and understand. We want to make it as easy as possible to improve your project’s scripts.

That’s why we developed…

scriptlint

The scriptlint logo

… as a CLI tool — think of a linter for your package scripts — with configurable rules and easily consumable documentation.

Here’s an example in a webpack based frontend project using scriptlint on the shell. Here’s the package file:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "name": "my-cool-project",
  "version": "1.0.0",
  …
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "eslint": "eslint src",
    "start-dev": "webpack-dev-server",
    "build": "webpack"
  },
  …
}

If you run scriptlint in this project, you will see a few warnings:

1
2
3
⧙։⧘ [warning] must contain a "start" script (mandatory-start)
⧙։⧘ [warning] must contain a "dev" script (mandatory-dev)
⧙։⧘ [warning] `test` script can't be the default script (no-default-test)

These are the warnings from the non-strict/minimum rules of scriptlint: your package.json must have scripts called dev, start and test and the latter one can’t be the default test script from npm init. So, here’s how we fix those issues:

  • Since “starting” the project would probably mean starting the dev server, we rename start-dev to start and alias dev to that. The other way around would also be fine.
  • Also, since we do not use any unit testing yet, we define test as a combination of “the code builds without errors” and “ESLint doesn’t complain” as the next best thing. We can add Jest and/or Cypress later, if we want but building, type checking and linting should be part of any test script chain.

Here’s how that looks now:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "my-cool-project",
  "version": "1.0.0",
  …
  "scripts": {
    "test": "npm run build && npm run eslint",
    "eslint": "eslint src",
    "start": "webpack-dev-server",
    "dev": "npm run start",
    "build": "webpack"
  },
  …
}

Run scriptlint again to check:

1
2
$ scriptlint
⧙։⧘ [✔️] ✨  All good

So now that we have the non-strict rules out of the way, let’s further improve this by running scriptlint --strict. Three new warnings come up:

1
2
3
⧙։⧘ [warning] script name "eslint" should start with one of the allowed namespaces (uses-allowed-namespace)
⧙։⧘ [warning] scripts must be in alphabetic order (alphabetic-order)
⧙։⧘ [warning] Use of unix double ampersand (&&) in script 'test' is not allowed, consider using npm-run-all/run-s (no-unix-double-ampersand)

Let’s start with the first 2 issues, because those are the easiest to fix — by running scriptlint --strict --fix that is! Some of the rules can be autofixed, in this case scriptlint will …

  • prepend the script named eslint with other:, the fallback namespace
  • sort the scripts alphabetically
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
⧙։⧘ [warning] Use of unix double ampersand (&&) in script 'test' is not allowed, consider using npm-run-all/run-s (no-unix-double-ampersand)
⧙։⧘ [✔️] Fixed 2 issues!

// results in …
{
  "name": "my-cool-project",
  "version": "1.0.0",
  …
  "scripts": {
    "build": "webpack",
    "dev": "npm run start",
    "other:eslint": "eslint src",
    "start": "webpack-dev-server",
    "test": "npm run build && npm run eslint"
  },
  …
}

Since other:eslint is not a great name, we will manually rename it to test:lint.

The last remaining issue (“Use of unix double ampersand (&&) in script ‘test’ is not allowed […]”) can be fixed by installing npm-run-all as a devDependency and rewriting test like this: run-s build test:lint. A quick check running scriptlint --strict --fix and there you go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ scriptlint --strict --fix
⧙։⧘ [✔️] ✨  All good

// end result:

{
  "name": "my-cool-project",
  "version": "1.0.0",
  …
  "scripts": {
    "build": "webpack",
    "dev": "npm run start",
    "start": "webpack-dev-server",
    "test": "run-s build test:lint",
    "test:lint": "eslint src"
  },
  …
}

A perfectly “scriptlint standard” compliant scripts section!

In this example we saw about half of the available rules in action. If you want to know what else scriptlint can do for you, check out our Github page! Of course scriptlint is open source (MIT license), available on npm, comes with JS module support, typescript typings, is configurable and extensible (custom rules!). It can be installed locally to your projects as well as globally to your shell. Here’s our installation guide.

We hope it helps your team with maintaining script quality and would love to hear your feedback. Maintaining the “standard”/scriptlint rules should be a community effort!

From Peerigon with ❤️

*Thanks to Tanner Hoisington and Johannes Ewald. *

npm scripts

Tooling

Linting

Code Quality Tools

One learns from mistakes. It's faster and easier is the Peerigon Workshops though.

to the programming workshops

Would you like to see some of our dev projects? Coming right up.

to our Success Stories

Wir sind Peerigon, eine Agentur für Softwareentwicklung.

Peerigon GmbH
Werner-von-Siemens-Straße 6
86159 Augsburg
+49 821 907 80 86 0

mail peerigon

service

Full-stack ConsultingSoftware DevelopmentProgramming WorkshopsTeam Support
BlogSuccess StoriesContactgo digital fundingWhy use TypeScript?

© 2021 Peerigon

Privacy PolicyLegal NoticePress
ClimatePartner