V našem týmu používáme už delší dobu správce NPM balíčků Yarn. Od verze 1.0, která už tu s námi nějaký čas je, můžeme používat funkci nazvanou “workspaces”. O co jde a proč by nás to mělo zajímat?
Yarn workspaces: O co jde?
Workspaces jsou koncept objevující se často v monorepech, což je architektura repozitáře, která obsahuje samostatně vyvíjené celky, které spolu ale mohou na sobě záviset. Závislé celky, v našem případě NPM balíčky mohou mít zároveň společně nějaký externí balíčky, na kterých závisejí, a my chceme samozřejmě optimalizovat strukturu node_modules
tak, aby se nám externí balíčky neduplikovaly a nevznikaly mezi nimi konflikty.
Pokud máme strukturu repozitáře, která vypadá jako na příkladu níže, musíme v adresáři my-package
i my-utils
spustit yarn install
, abychom pro oba balíčky nainstalovali jejich závislosti. Tím ale nevyřešíme závislost my-package
balíčku na my-utils
.
.git
my-package/
package.json # obsahuje dependencies: { lodash: 4, my-utils: * }
node_modules/
lodash/
my-utils
package.json # obsahuje dependencies: { lodash: 4 }
node_modules/
lodash/
Tady nám k pomoci přicházejí právě Yarn a jeho workspaces. Stačí v rootu repozitáře vytvořit package.json
, ve kterém workspaces nadefinujeme, a na tom samém místě spustit yarn install
.
{
"private": true,
"workspaces": ["my-package", "my-utils"]
}
Nyní bude struktura repozitáře vypadat takto:
.git
package.json
my-package/
package.json # obsahuje dependencies: { lodash: 4, my-utils: * }
my-utils
package.json # obsahuje dependencies: { lodash: 4 }
node_modules
lodash/
my-utils/ # symlink to ../my-utils
Yarn použil techniku zvanou hoisting a závislosti obou balíčků (v našem příkladě lodash
) posunul v adresářové struktuře výše do node_modules
v rootu repozitáře.
Také zjistil, že my-package
závisí na balíčku my-utils
, přičemž jeden z nadefinovaných workspaců je balíček se stejným názvem. Proto my-utils
nenainstaloval z npm registru balíčků, ale vytvořil v node_modules
filesystémový odkaz na tento workspace (adresář).
Uvnitř balíčku my-package
můžeme teď naimportovat a použít my-utils
stejně jako kdybychom jej instalovali z NPM registrů pomocí yarn add
.
// my-package/src/index.js
import * as utils from 'my-utils';
Výše uvedených vlastností využívají nejznámější javascriptové nástroje a knihovny jako třeba Babel, React.js nebo Storybook pro správu svých monorepozitářů.
Jak využíváme workspaces v Ackee
V Ackee rádi tvoříme Open source a Yarn workspaces využíváme právě v případě, že se některý z balíčků rozroste do většího ekosystému, který je spravovaný v monorepu, jako třeba Antonio – nadstavba nad fetch API s utilitami a bindingy do jiných nástrojů, které běžně při vývoji používáme.
V těchto případech se běžně využívá zavedená konvence adresáře packages
, který obsahuje všechny hlavní balíčky:
.git
packages/
my-package/
my-utils/
node_modules/
přičemž seznam workspaců se poté nedefinuje jako seznam těchto balíčků:
"workspaces": ["packages/my-package", "packages/my-utils"]
ale zjednodušeně jen pomocí wildcard zápisu:
"workspaces": ["packages/*"]
Ale i v případě, kdy vyvíjíme open source balíček, který je jednoduchý, nepotřebuje složitý ekosystém a víme tedy, že bude jen jeden, se Yarn workspaces mohou hodit.
Dobrý vývojář dbá na kvalitu kódu (open source kódu dvojnásob 😁), a proto – pokud vyvíjíme nějaký nový NPM balíček – si ho chceme ještě před publishem (nebo při vývoji nějaké nové featury) vyzkoušet. A to ideálně pomocí importu z node_modules
, stejně jako to budou dělat všichni, kdo si ho nainstalují z NPM registrů.
V takovém případě využíváme často architekturu, kdy náš balíček my-simple-packge
přesuneme v repozitáři do stejně pojmenované složky a vytvoříme workspace example
, který na balíčku závisí:
.git
package.json
example
package.json # obsahuje dependencies: { my-simple-package: * }
my-simple-package
package.json
node_modules/
Jak už určitě tušíte, definice workspaces v root package.json
u repozitáře bude:
"workspaces": ["my-simple-package", "example"]
a uvnitř adresáře (workspacu) example
pak můžeme vyzkoušet, zda import a použití balíčku funguje tak, jak si představujeme:
// example/src/index.js
import { ... } from 'my-simple-package'
Dříve jsme pro tento účel využívali yarn_link
případně později spíše yalc
což je skvělý nástroj pro přilinkování lokální verze nějakého balíčku do existujícího projektu. Pro odzkoušení balíčku v izolaci jsme ale často naráželi na některé nevýhody týkající se hoistingu závislostí a více node_modules
adresářů s více verzemi balíčků. Pokud totiž použijete přilinkování balíčku, skončíte s následující architekturou:
example
package.json # obsahuje dependencies: { my-simple-package: * }
node_modules/
my-simple-package/ # symlinked using yarn link to ../../my-simple-package
my-simple-package
package.json
node_modules/
Při buildu example
pak můžete narazit na špatné vyresolvování závislostí, případně konflikt, protože se berou z dvou oddělených node_modules
.
Yarn workspaces s použitím hostingu do top level node_modules
se zdají v tomto případě jako mnohem lepší řešení…
Závěrem
Workspaces se v komunitě staly tak rozšířenou a používanou funkcí, že originální správce balíčků npm
ji od verze 7 podporuje již také, a není tak nutností použití Yarnu.
Všechny uvedené příklady jsou samozřejmě značně zjednodušené a rozhodně nepodchycují všechny výhody i kompromisy, které na vás při použití mohou čekat. Pro ukázání principů a příkladů jejich využití nám ale posloužily dobře.
Workspaces jsou podle me skvělým případem nástroje, který perfektně slouží jednomu účelu, kterým je správa a orchestrace externích a interních závislostí, a dělá to dobře. Proto je lze krásně zkombinovat s dalšími tooly a tvořit tak robustní dev tech stacky.