Jak předcházet chaotické organizaci npm skriptů
Řešíte, jak organizovat skripty napříč projekty přehledně a konzistentně? Inspirujte se systémem, který zjednoduší práci vám i kolegům.
Při práci na kterémkoli projektu v javascriptovém ekosystému se setkáváme s opakujícím se problémem: jak se bude daný projekt ovládat?
Řeč je samozřejmě o npm skriptech, které již plně nahradily nástroje jako byl Grunt, Gulp či Yeoman.
npm skripty umí definovat řadu užitečných příkazů, které se hodí třeba ke kontrole testů, spuštění lokálního vývoje nebo při nasazování do produkce. Problémem zůstává, že každý vývojář si dané skripty pojmenovává po svém, a to vývojářům mírně komplikuje přecházení mezi projekty. Každý projekt má vlastní sadu příkazů, které je potřeba znát, neboť neexistuje jednotná sada skriptů, která by zaručovala, že se projekty alespoň v základu budou stejně ovládat.
Jak se zorientovat v npm skriptech
Formát JSON, a tím pádem i package.json
samotný, nám neposkytuje moc prostředků, jak samotné skripty vhodně pojmenovávat nebo organizovat.
Samotné zavolání příkazu npm -h
(pozn. red. – pro stručnost uvádíme v článku pouze příklady s jedním správcem balíčků, a to npm) vypíše pouze nápovědu s dostupnými příkazy daného správce balíčků, nikoli informaci o dostupných příkazech v projektu.

npm -h
Naštěstí i na tohle vývojáři package managerů mysleli, a tak díky npm run
můžeme vypsat dostupné npm příkazy – včetně scriptů volaných příkazem. Jenže co když nechceme trávit čas dekódováním skriptů a chceme přesně vědět, co se provede?

Kéž by byla možnost, jak dané příkazy lépe popsat… Ku příkladu v Makefile lze pomocí komentářů vytvořit samodokumentující nápovědu o dostupných příkazech. Funguje to celkem jednoduše: každý příkaz má komentář, který je možné vypsat jako nápovědu v konzoli.

Bohužel, ani něco tak jednoduchého v případě package.json
nejde. Pomohlo by, kdyby každému příkazu bylo možné přiřadit vlastní komentář, ale JSON formát toto neumožňuje.
Doba se však vyvíjí a specifikace JSON5 či JSONC již komentáře povolují. I přes navržená RFC pro komentáře či RFC pro JSON5 nebo dotazy ohledně podpory JSON5, npm zatím stále tyto požadavky ze stran vývojářů nebere v potaz a odvolává se, že Node.js také nepodporuje JSON5. A tak stále není moc cest, jak dostat package.json
do čitelnější podoby. Jediným řešením tak je nastavení jmenných konvencí při pojmenovávání npm skriptů a jejich dodržování.
Jmenné konvence
V jednom pull requestu jsme s kolegy řešili, že bychom potřebovali jeden příkaz, který by nám zkontroloval celý projekt. Názvy jako all
, ci
či check
zprvu působily slibně, avšak:
all
vychází z lokálního použití v PHP projektech, ale už samotný název je matoucí. Co „všechno“ by příkaz měl spouštět?ci
simuluje CI pipeline, s čímž už npm počítalo, poněvadž existuje nativní příkaznpm ci
.check
může být matoucí při spouštění unit testů a v kontextu Yarn je již rezervováno.
Brzy jsme zjistili, že není co vymýšlet. Vrátíme-li se o pár kroků zpět, zjistíme, že nápovědu dostáváme již při zavolání příkazu npm init
, který generuje jediný příkaz a tím je npm test
. A to bylo ono. 🎉

npm init
a jediný existující skript se jmenuje test
„It's far more convenient to know I can rely onnpm test
in all projects than having to remember different testing task names across different projects. It's a part of the standard set of tasks: the built-ininstall
(orci
), thenbuild
,test
, andstart
.“
– Adam Kudrna (2021)
A tím bylo rozhodnuto. Každý projekt bude minimálně obsahovat příkaz test
, který bude testovat celý repozitář – provede unit testy, lintování, kontroly typů, aj.
npm má kromě test
zavedené i další příkazy.
A tak se nám začala formovat první pravidla:
start
– pro spuštění projektu,stop
– pro zastavení projektu,test
– pro otestování projektu.
Jmenné prostory a aliasy
K ovládání celého projektu je potřeba více příkazů než jen start
, stop
a test
. Drtivou většinu projektů je nutné navíc sestavit (build
), provést validaci kódu (lint
), zkontrolovat formátování (format
), typy (types
– v případě TypeScriptu), nemluvě o unit či end-to-end testech. Tím se potřeba vhodného pojmenování značně umocňuje.
Nač vymýšlet kolo, když je možné se inspirovat u větších a dobře zaběhlých projektů. Za zmínku zde stojí Bootstrap, který má příkazy rozdělené podle kontextu a jako oddělovač používá dvojtečku :
. Definuje tím „namespace“, kde nejkratší příkaz spouští přidružené příkazy, které se ve jmenném prostoru vyskytují.
Při použití pomlčky jako oddělovače bychom mohli narazit na problém víceslovných příkazů, které se ztrácejí v kontextu:
"format-changelog-design-system":
// versus:
"format:changelog:design-system":
Proto se dvojtečka ukazuje jako lepší volba.
Pravidla pro sestavení příkazů
Již víme, že každý projekt by měl obsahovat příkaz test
. Předpokládáme, že v rámci něj dojde k otestování celého projektu – unit a end-to-end testy, statická analýza lintery a formátovači, kontrola typů, atp.
"test": // testování celého projektu (test:unit, lint, types, format)
"test:unit": // kontrola unit testů
"test:e2e": // end-to-end testy
"lint": // lintování kódu
"lint:scripts": // lintování kódu, resp. skriptů
"lint:scripts:fix": // lintování kódu, resp. oprava skriptů
"format": // kontrolu formátu (Prettier)
"types": // kontrolu typů
S výše zmíněnými pravidly je možné libovolně škálovat strukturu příkazů. Pokud v rámci pravidel definujeme příkazy, které se v projektech budou vždy vyskytovat, lze přecházet z projektu na projekt s jistotou, že základní sada příkazů bude neměnná.
Celá sada pak může vypadat jako na příkladu níže. 👇
"dev": start the development
"start": start the production
"build": build the project
"clean": clean build files and other things
"test": main script for testing entire project
"test:unit": run unit tests
"test:unit:ci": run unit tests for CI
"test:unit:local": run unit tests on local
"test:unit:watch": run unit tests in watch mode
"test:unit:coverage": run unit tests with code coverage
"lint": main linting script
"lint:scripts": run ecma script linting
"lint:scripts:fix": run ecma script fixing
"lint:commit": lint commit
"lint:markdown": lint markdown
"lint:text": lint text
"lint:text:fix": fix text
"format": run format checker
"format:fix": run format fixer
"types": run type checking
Kompletní pravidla:
- název namespace je oddělován pomocí
:
, - víceslovné výrazy jsou oddělovány pomocí
-
, - namespace je aliasem na spuštění sdružených příkazů.
Seznam povinných příkazů:
test
– testování celého projektu,test:unit
– kontrola unit testů,lint
– statická analýza kódu lintery,format
– kontrola formátování,types
– kontrola typů,build
– sestavení projektu pro produkci,dev
– spuštění lokálního vývoje,start
– spuštění produkční konfigurace,release
– příprava nového releasu,deploy
– nasazení projektu (na produkci, staging, atp.).
Spouštění skupiny příkazů
Vzhledem k výše popsanému pojmenovávání příkazů a jejich slučování do jmenných prostorů potřebujeme tyto příkazy umět spouštět paralelně či sériově. K tomu lze přistoupit dvěma způsoby.
Využití operátoru ;
, &&
nebo &
První možností je využití shell operátorů jako jsou ;
a &&
pro sériově spuštění příkazů anebo &
v případě paralelního spuštění.
Paralelní spuštění příkazů
"lint": "lint:scripts & lint:css & lint:html"
Řešení je to bezesporu čitelné, ale bohužel může způsobit nechtěné patálie. Při použití &
dochází k vytváření subprocesu, který způsobí, že původní npm
proces nedokáže říct, jestli subproces doběhl bez chyby či nikoliv. To může být problém obzvláště u dlouho běžících skriptů nebo když potřebujeme optimalizovat integrační pipeliny.
Sériové spuštění příkazů
"lint": "lint:scripts; lint:css; lint:html"
I v tomto případě můžeme narazit na nechtěné chování. Následující příkaz se spouští nehledě na návratovou hodnotu toho prvního. Tedy přestože první příkaz vrátí chybu, následující příkazy se i tak spustí.
Pokud toto chování chceme změnit, máme k dispozici &&
. Pokud první příkaz selže s jiným návratovým kódem než 0
(což znamená úspěšné dokončení), tak se další příkaz již nespustí.
"lint": "lint:scripts && lint:css && lint:html"
Využití balíčku npm-run-all
K dispozici je druhá možnost, která mj. řeší i problém zmíněný výše, a to použití balíčku npm-run-all
, resp. npm-run-all2
.
npm-run-all
projekt neudržuje, proto vznikl jeho udržovaný fork npm-run-all2
.Balíček nabízí dva skripty:
run-p
pro paralelní zpracování příkazů:
"lint": "run-p lint:scripts lint:css lint:html"
run-s
pro sériové zpracování příkazů:
"lint": "run-s lint:scripts lint:css lint:html"
Pro lepší čitelnost skriptů může být vhodnější používat plný název npm-run-all
s přepínači --parallel
nebo --serial
. Zápis sice není úsporný, ale za to explicitnější.
Další výhodou npm-run-all
je, že umí používat zástupný znak *
pro nahrazení skupiny výrazů. Díky jmenným prostorům můžeme skupinu příkazů:
"lint": "npm-run-all --serial lint:scripts lint:css lint:html"
zjednodušit na:
"lint": "npm-run-all --serial lint:*"
A co life cycle hooks?
Je vítanou vlastností npm, že při spouštění jakéhokoli příkazu provede i takzvané life cycle hooks, neboli pre
a post
skripty. Nespustí se pouze příkaz samotný (v našem případě lint
), ale před ním i prelint
, resp. po něm postlint
– za předpokladu, že jsou příkazy definované.
Příkazy se provedou následovně:
- (
prelint
), lint
,- (
postlint
).
Je nutné zmínit jedno velké „ale“. U příkazů jako jsou start
a prestart
nebo serve
a jeho hook preserve
dochází k úplné změně významu. Na základě těchto problémů Yarn úplně odebral podporu pro pre
a post
hooky u většiny skriptů. 👇
In particular, we intentionally don't support arbitrarypre
andpost
hooks for user-defined scripts (such asprestart
). This behavior caused scripts to be implicit rather than explicit, obfuscating the execution flow. It also sometimes led to surprising behaviors, likeyarn serve
also runningyarn preserve
.
– Lifecycle Scripts, Yarn
Otázkou však zůstává: Pokud přicházíme o podporu pre
a post
hooků, jak pojmenovávat příkazy, aby byly explicitní?
Řešení je nasnadě, i když vyžaduje trochu více psaní. pre
lze nahradit za prepare
a post
za finalize
.
Posloupnost pak vypadá následovně:
build:prepare
build
build:finalize
V kombinaci s výše uvedenými pravidly pro jmennou konvenci a způsoby pro spouštění příkazů můžeme definovat npm skripty pro build
tímto způsobem:
"build": "npm-run-all --serial build:prepare build:compile build:finalize"
"build:prepare": "shx rm -rf dist"
"build:compile": "rollup"
"build:finalize": "mv package.json dist/"
Řazení skriptů
Čitelnost skriptů lze umocnit pomocí správného řazení skriptů v package.json
. Nejjednodušší volbou se může jevit abecední řazení, které ale v důsledku nemusí být příliš praktické.
V praxi je efektivnější řadit skripty podle kontextu. Jako příklad může posloužit příkaz build
zmíněný výše. Na první místo se uvádí alias (hlavní volání celého kontextu) a na následujících řádcích jsou použité skripty.
Celé bloky je možné řadit buď dle abecedy, anebo, což je často lepší řešení, podle četnosti použití skriptů vývojářem. Ve většině případů je při práci s novým projektem klíčové co nejjednodušší a nejrychlejší nastartování vývoje. Proto se příkazy jakostart
nebo dev
umisťují na začátek. Následují příkazy důležité pro vývoj jako test
, lint
, types
, a mezi posledními bývá build
a deploy
. Na konec patří obslužné a pomocné skripty, které nepatří do žádného kontextu.
Celé to pak může vypadat jako v package.json
design systému Spirit.
Závěrem
Pojmenovávat npm skripty a postavit jejich logiku tak, aby jim někdo další rozuměl, je opravdu těžké. Přitom vhodně zvolená organizace příkazů zlepšuje nejen čitelnost a udržitelnost projektů, ale také usnadňuje spolupráci v týmu a přechod mezi různými projekty.
Systém představený v tomto článku by měl být robustní a udržitelný jak z hlediska pojmenovávání, tak i jejich spouštění. Stačí následovat tato doporučení:
- Konzistentní jmenné konvence využívající oddělovače
:
a-
. - Vytváření jmenných prostorů pro lepší organizaci skriptů.
- Používání aliasů pro zjednodušení spouštění skupin skriptů.
- Efektivní využití nástrojů jako
npm-run-all
pro paralelní a sériové spouštění skriptů. - Nahrazení
pre
apost
hooků explicitnějšími názvy jakoprepare
afinalize
.
Přestože současná omezení formátu JSON v package.json
představují výzvu, situace se snad postupem času zlepší. Do té doby si musíme vystačit s pochopitelným a explicitním pojmenováváním. Ať už zvolíte kterékoliv řešení, klíčem k úspěchu je konzistence, čitelnost a snadná škálovatelnost.
Související odkazy
- Bootstrap a jeho package.json – Příklad rozsáhlého projektu s dobře organizovanými skripty (GitHub)
- A NPM package Script Strategy – Další pohled na strategii organizace npm skriptů (Medium, Zachary Leighton, 2018)
- NPM Scripts: Tips Everyone Should Know – Užitečné tipy pro práci s npm skripty (Corgi Bytes, Kamil Ogórek, 2017)
Anglickou verzi článku najdete na webu autora.
Další články od autora:
Sdílejte
Líbí se vám článek? Podpořte jej sdílením!
Komentujte
Chcete k článku něco doplnit? Našli jste chybu? Napište e-mail.