Ha a package.json-odban van egy build vagy test szkript, valószínűleg soha nem gondolkodtál el azon, mennyi idő megy el azzal, hogy az npm run egyáltalán elindítja a Node-ot, ami aztán elindítja a te szkriptedet. Ez a felesleges kör egy triviális szkript esetén nagyságrendekkel több időt visz el, mint maga a munka. A Node.js 22 óta van erre egy beépített, npm nélküli megoldás – csak épp nem ártalmatlan cserét ajánl.

Mi az a node --run?
A Node.js CLI dokumentációja szerint a --run kapcsoló a package.json scripts mezőjéből futtat le egy megnevezett parancsot – gyakorlatilag az npm run egy alternatívája, csomagkezelő nélkül. A funkciót Yagiz Nizipli implementálta, a nodejs/node#52190 PR-ban, és a Node.js 22.0.0-ban jelent meg 2024. április 24-én.
$ node --run test
$ node --run build -- --watch
Az argumentumok a -- után egyszerűen hozzáfűződnek a szkripthez, ugyanúgy, ahogy az npm run-nál megszoktuk. Eddig semmi meglepő. A különbség ott kezdődik, hogy mi történik a motorháztető alatt.
A sebességkülönbség valódi, és mérhető
Az npm run minden egyes híváskor elindít egy npm CLI-t, ami beolvassa a konfigurációt, felépíti a környezeti változók egy egész csokrát, majd shell-en keresztül elindítja a tényleges parancsot. A node --run ezt megkerüli: a Node bináris maga olvassa be a package.json-t, és közvetlenül futtatja a parancsot. Ezt nem elhiszem-alapon állítom – lefuttattam helyben, Node 22.23.2-n, egy 20 iterációs mérésben egy szinte semmit nem csináló szkriptre (node -e "process.exit(0)"):
--- npm run hello x20 ---
real 0m5.195s (≈260 ms / hívás)
--- node --run hello x20 ---
real 0m0.509s (≈25 ms / hívás)
Tízszeres különbség egy olyan szkriptnél, ami gyakorlatilag semmit nem csinál – vagyis a mért idő szinte tisztán az indítási overhead. Ez nem számít sokat, ha egyszer futtatod le a build-et. De egy monorepóban, ahol egy CI pipeline percenként tucatnyi run hívást indít workspace-enként, vagy egy fájlváltozásra újrainduló watch-szkriptnél ez a különbség összeadódik. Ez a motiváció is áll a funkció mögött: a dokumentáció kifejezetten „top performance for the most common use cases” célt említ.
Amit szándékosan kihagy – és csendben teszi
A dokumentáció egy külön szakaszt szentel ennek, „Intentional limitations” címmel. Két dolgot említ explicit módon: nem futtatja le a pre/post életciklus-szkripteket, és nem állítja be a csomagkezelőre jellemző környezeti változókat. Ez így, önmagában absztraktnak hangzik – úgyhogy leteszteltem, mit jelent ez a gyakorlatban.
Egy pretest/test/posttest hármassal az npm run test mindhármat lefuttatja sorban – ezt az npm dokumentációja is így írja le. A node --run test viszont csak a test szkriptet futtatja le, a pretest és posttest szó nélkül kimarad – nincs figyelmeztetés, nincs hibaüzenet, egyszerűen nem fut le. Ha valaki a pretest-re bízza a lint vagy típusellenőrzés lefuttatását, és csak lecseréli a CI-ban az npm run test-et node --run test-re a sebesség kedvéért, az a lépés némán kiesik a pipeline-ból.
A másik, ennél is alattomosabb rész: az npm egy sor npm_* előtagú környezeti változót állít be minden szkript futtatásakor – ilyen a npm_package_version vagy a npm_lifecycle_event, amiket rengeteg build-szkript használ verziószám vagy release-címke beégetéséhez. Ezt is leteszteltem ugyanazzal a package.json-nal:
--- npm run showvars ---
npm_package_version=1.2.3
npm_lifecycle_event=showvars
--- node --run showvars ---
npm_package_version=undefined
npm_lifecycle_event=undefined
Na, ezt nem tudtam: a node --run alatt ezek a változók nem hiányoznak hibával, hanem csendben undefined-ra állnak. Egy Docker image-taggelő vagy Sentry release-jelölő szkript, ami process.env.npm_package_version-re épít, nem fog elszállni – csak egy undefined stringgel taggeli meg a build-et. Ez pontosan az a fajta hiba, ami nem esik le code review-n, csak élesben, amikor már valaki a hibás verziójelzés miatt keresi a rossz image-et.
Amit mégis kapsz cserébe
A hiányzó npm-változók helyett a Node.js 22.3.0-tól (nodejs/node#53032 és #53058) két saját változót ad: NODE_RUN_SCRIPT_NAME (a futtatott szkript neve) és NODE_RUN_PACKAGE_JSON_PATH (a feloldott package.json elérési útja). Ez helyben is így viselkedik:
$ node --run test
TEST-RAN
NODE_RUN_SCRIPT_NAME=test
NODE_RUN_PACKAGE_JSON_PATH=/tmp/noderun-test/package.json
Ez nem pótolja az npm teljes változókészletét, de a leggyakoribb esetre – „melyik szkript fut, honnan” – ad egy stabil, dokumentált API-t.
A másik hasznos, kevésbé reklámozott képesség ugyanabból a release-ből (nodejs/node#53154): a --run felfelé bejárja a könyvtárfát a gyökérig, amíg talál egy package.json-t, és minden érintett szülőkönyvtár node_modules/.bin mappáját hozzáfűzi a PATH-hoz. Ez konkrétan monorepókban számít: egy workspace-csomagból kiadott node --run hívás eléri a gyökér node_modules/.bin-jébe hoisztolt bináris eszközöket is, akkor is, ha te magad egy mélyebb alkönyvtárból dolgozol. Ezt is leteszteltem – egy gyökér node_modules/.bin-be tett bináris egy két szinttel lejjebb lévő csomag szkriptjéből simán elérhető volt node --run-nal, közvetlen PATH-módosítás nélkül.
Egy csendes, releváns rész: az --env-file
Ha a --env-file kapcsolóval töltesz be környezeti változókat, és utána --run-nal futtatsz egy szkriptet, a dokumentáció külön kiemeli: az onnan betöltött változók nem öröklődnek a --run által indított folyamatba. Ezt is leellenőriztem – egy .env fájlból betöltött változó a --run-nal indított szkriptben undefined maradt, miközben a node --env-file önmagában (egy sima node script.js előtt) helyesen belövi. Ha valaki lokális titkokat egy .env fájlból akar egy npm-szkriptnek átadni --run-on keresztül, csalódni fog.
Mikor van értelme, mikor nem
A node --run ott ér igazán sokat, ahol sok, gyakran ismételt, egyszerű szkriptindítás történik: fájlváltozásra újrainduló dev-loopban, vagy egy CI mátrixban, ahol ugyanaz a lépés workspace-enként sokszor lefut. Ott a 200+ milliszekundumos npm-overhead valódi, összeadódó költség.
Éles projektben én csak ott cserélném le vele az npm run-t, ahol előbb végigmentem a szkriptláncon, és tudom, hogy: nincs pre/post hook, amire számítok; a szkript nem használ npm_* változót; és nem .env-ből várja a titkait. Ez a három feltétel egy egyszerű build- vagy teszt-szkriptnél könnyen teljesül, egy örökölt, sok éve épülő npm-szkript-erdőnél viszont pont a kivétel szokott lenni a szabály. A dokumentáció maga is világosan leszögezi: ez szándékosan nem az npm run (vagy más csomagkezelők run parancsának) helyettesítője, hanem egy szűkebb, gyorsabb eszköz egy szűkebb feladatra. Ha ezt a határvonalat tiszteletben tartod, jó eszköz; ha vakon lecseréled vele a CI-t, a hiba nem a build logban jelenik meg, hanem egy hibás verziójelzésű image-ben vagy egy lemaradt lint-lépésben, jóval később.
Források
- Node.js CLI dokumentáció –
--run - nodejs/node#52190 – a
--runimplementációja - nodejs/node#53032 –
NODE_RUN_SCRIPT_NAME - nodejs/node#53058 –
NODE_RUN_PACKAGE_JSON_PATH - nodejs/node#53154 – gyökér package.json bejárás és PATH-bővítés
- npm dokumentáció – scripts (pre/post életciklus)
- Node.js 22.0.0 release blogbejegyzés (2024-04-24)