<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/stylesheet.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	
	
	<link href="https://blog.comandeer.pl/feeds/kategorie/eksperymenty.xml" rel="self" type="application/atom+xml"/>
	<link href="https://blog.comandeer.pl/kategorie/eksperymenty" rel="alternate" type="text/html"/>
	<updated>2026-08-10T16:09:14.719Z</updated>
	<id>https://blog.comandeer.pl/feeds/kategorie/eksperymenty.xml</id>
	<title type="html">Comandeerowy blog</title>
	
		<category term="Eksperymenty"/>
		<subtitle>Kategoria Eksperymenty</subtitle>
	
	
		
	
	
		<entry>
			<title type="html">Kosmiczna zabawa</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/kosmiczna-zabawa" rel="alternate" type="text/html"/>
			<published>2025-09-14T22:23:00.000Z</published>
			<updated>2025-09-14T22:23:00.000Z</updated>
			<id>https://blog.comandeer.pl/kosmiczna-zabawa</id>
			
				<summary><![CDATA[Jak to chciałem użyć pewnej biblioteki komponentów, ale po swojemu]]></summary>
			
			<content type="html"><![CDATA[<p>Ostatnio trafiłem na projekt <a href="https://www.cosmic-ui.com/" rel="noreferrer noopener">Cosmic UI</a> – biblioteki gotowych komponentów UI utrzymanych w stylistyce sci-fi. Stwierdziłem wówczas, że wygląda to całkiem interesująco i w sumie fajnie byłoby użyć tego do jakiegoś małego projekciku. Wówczas nie wiedziałem jeszcze, na co się piszę…</p>
<h2 id="początkowe-rozczarowanie"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#początkowe-rozczarowanie">Początkowe rozczarowanie</a></h2>
<p>Jak przystało na porządnego użytkownika dowolnego frameworka, ochoczo kliknąłem przycisk <i lang="en">Get started</i>. Jednak w miarę czytania mój entuzjazm powoli opadał. Pierwszym problemem był wykorzystany technologiczny stos. Cosmic UI opierało się na <a href="https://ark-ui.com/" rel="noreferrer noopener">Ark UI</a>, <a href="https://react.dev/" rel="noreferrer noopener">Reakcie</a> i <a href="https://tailwindcss.com/" rel="noreferrer noopener">Tailwindzie</a>, z dodatkiem <a href="https://zagjs.com/" rel="noreferrer noopener">Zaga</a> do bardziej złożonych komponentów. Zatem na technologiach, których raczej nie użyłbym do małego, pobocznego projektu. Jednak o wiele większym problemem był fakt, że… Cosmic UI nie było pakietem npm.</p>
<p>Żeby zainstalować dowolny komponent, należy <em>skopiować</em> jego kod z dokumentacji i wkleić do swojego projektu. Niemniej komponenty zależą od siebie nawzajem i żeby mieć <a href="https://www.cosmic-ui.com/docs/button" rel="noreferrer noopener">ładne przyciski</a>, muszę tak naprawdę najpierw zainstalować <a href="https://www.cosmic-ui.com/docs/frame" rel="noreferrer noopener">komponent ramki</a>. A żeby zainstalować komponent ramki, muszę dokładnie przeczytać jego dokumentację, bo przy okazji wymaga on instalacji paczki do <a href="https://www.npmjs.com/package/@left4code/svg-renderer" rel="noreferrer noopener">renderowania SVG</a>. W tym momencie zapaliła mi się&nbsp;czerwona lampka, ale postanowiłem ją zignorować. W końcu to naprawdę są ładne komponenty, nie może być aż tak źle, prawda?</p>
<h2 id="odwrócona-inżynieria-wsteczna"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#odwrócona-inżynieria-wsteczna">Odwrócona inżynieria wsteczna</a></h2>
<p>Skoro nie mogłem użyć Cosmic UI w jego oryginalnej postaci, stwierdziłem, że zrobię to <s>dobrze</s> po swojemu. Co w tym miejscu oznacza, że zaglądnę w bebechy biblioteki i znajdę, w jaki sposób generowane są te fancy komponenty. W tym celu posłużyłem się niezwykle zaawansowanym narzędziem, służącym do inżynierii wstecznej aplikacji webowych – <a href="https://developer.chrome.com/docs/devtools/inspect-mode" rel="noreferrer noopener">inspektorem elementów w Chrome</a>. Dzięki niemu odkryłem, że sztuczka, wbrew pozorom, jest dość&nbsp;prosta. Za wygląd komponentów odpowiadają grafiki SVG, na które następnie naniesione zostały pozostałe elementy interfejsu. W uproszczeniu kod każdego komponentu wygląda mniej więcej tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"component"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"component__background"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;[…]&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"component__content"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;[…]&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Element <code>.component</code> zawiera w sobie dwa elementy: grafikę SVG oraz kontener z treścią komponentu. Cały komponent jest pozycjonowany relatywnie, natomiast sama grafika – absolutnie. Dzięki temu może być użyta jako tło dla komponentu. Tę technikę (przy użyciu <code>div</code>a zamiast SVG) zobaczyć można na poniższym przykładzie:</p>
<div><embed- src="https://codepen.io/Comandeer/pen/VYvJNVE"><p class="embed-fallback"><a href="https://codepen.io/Comandeer/pen/VYvJNVE" rel="noreferrer noopener">Przejdź bezpośrednio do osadzonej treści na CodePenie.</a></p></embed-></div>
<p>Skoro więc sztuczka jest tak prosta, wystarczy wyciągnąć&nbsp;SVG z kodu i pora na CS-a… znaczy na poboczny projekt. Tylko pojawił się pewien problem: w <a href="https://github.com/rizkimuhammada/cosmic-ui" rel="noreferrer noopener">repozytorium Cosmic UI</a> nie było żadnego pliku SVG. To była druga czerwona lampka, którą zignorowałem. Zacząłem więc szperać w kodzie. Zauważyłem, że wszystkie komponenty używają komponentu <code>Frame</code>. On sam z kolei opisany był jako służący do wyświetlania niestandardowych ramek SVG dla pozostałych komponentów. Spojrzałem na przykład użycia:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Frame</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"drop-shadow-2xl drop-shadow-primary/50"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  paths</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    '[{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-1-stroke)","fill":"var(--color-frame-1-fill)"},"path":[["M","37","12"],["L","0% + 59","12"],["L","0% + 85","0% + 33"],["L","79","0% + 12"],["L","50% - 3","12"],["L","50% + 16","30"],["L","100% - 35","30"],["L","100% - 16","47"],["L","100% - 16","100% - 47.05882352941177%"],["L","100% - 8","100% - 44.85294117647059%"],["L","100% - 9","100% - 16.666666666666668%"],["L","100% - 17","100% - 14.705882352941176%"],["L","100% - 17","100% - 30"],["L","100% - 34","100% - 12"],["L","50% + 13","100% - 12"],["L","50% + 15","100% - 26"],["L","50% - 11","100% - 12"],["L","37","100% - 12"],["L","19","100% - 30"],["L","19","0% + 50.490196078431374%"],["L","10","0% + 48.529411764705884%"],["L","10","0% + 20.098039215686274%"],["L","0% + 19.000000000000004","0% + 18.38235294117647%"],["L","19","29"],["L","37","12"]]},{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-2-stroke)","fill":"var(--color-frame-2-fill)"},"path":[["M","50% + 10","15"],["L","50% + 19","15"],["L","50% + 24","0% + 20"],["L","50% + 16","0% + 20"],["L","50% + 10","15"]]},{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-3-stroke)","fill":"var(--color-frame-3-fill)"},"path":[["M","50% + 25","15"],["L","50% + 34","15"],["L","50% + 40","0% + 21"],["L","50% + 31","0% + 21"],["L","50% + 25","15"]]},{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-4-stroke)","fill":"var(--color-frame-4-fill)"},"path":[["M","50% + 40","15"],["L","50% + 52","15"],["L","50% + 61","0% + 23"],["L","50% + 49","0% + 23"],["L","50% + 40","15"]]},{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-5-stroke)","fill":"var(--color-frame-5-fill)"},"path":[["M","36","3"],["L","0% + 58","0"],["L","0% + 84","0% + 40"],["L","81","0% + 0"],["L","50% - 1","4"],["L","50% + 5","6"],["L","50% + 54","7"],["L","50% + 74","23"],["L","100% - 32","21"],["L","100% - 8","42"],["L","100% - 9","100% - 52.450980392156865%"],["L","100% + 0","100% - 50.245098039215684%"],["L","100% + 0","100% - 15.196078431372548%"],["L","100% - 7","100% - 13.480392156862745%"],["L","100% - 7","100% - 27"],["L","100% - 29","100% - 3"],["L","50% + 14","100% + 0"],["L","50% + 21","100% - 31"],["L","50% - 13","100% + 0"],["L","37","100% - 4"],["L","11","100% - 28"],["L","10","0% + 55.3921568627451%"],["L","0","0% + 52.94117647058823%"],["L","1","0% + 18.627450980392158%"],["L","11","0% + 16.666666666666668%"],["L","11","25"],["L","36","3"]]}]'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  )}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">/&gt;</span></span>
<span class="line"></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>I wówczas zrozumiałem, czemu w całym repozytorium nie było plików SVG. Te były bowiem dynamiczne renderowane dla każdego komponentu osobno przy pomocy wspomnianego już&nbsp;wcześniej renderera SVG, pakietu npm <code>@left4code/svg-renderer</code>. Każdy komponent dostawał tablicę <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/d#path_commands" rel="noreferrer noopener">ścieżek SVG</a>, które następnie były mielone do faktycznych SVG. Co więcej, sposób działania biblioteki oznaczał, że każdy komponent, który trafiał ostatecznie do przeglądarki, miał swoją kopię takiego SVG. Zatem jeśli na stronie było 50 przycisków, oznaczało to, że każdy z tych przycisków ma swoje SVG. Wisienką na torcie jest fakt, że taka tablica ścieżek jest ciągiem tekstowym, wsadzonym do <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse" rel="noreferrer noopener"><code>JSON.parse()</code></a>, co – bez żadnego wyraźnego powodu – sprawia, że całość jest całkowicie niesformatowana w edytorze kodu.</p>
<p>To była trzecia czerwona lampka i fakt, że ją zignorowałem, skłania mnie do refleksji, czy aby nie jestem masochistą. Postanowiłem bowiem <em>wyciągnąć</em> te ścieżki z kodu i stworzyć z nich pliki SVG samodzielnie.</p>
<h2 id="ramki-zagłady"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#ramki-zagłady">Ramki zagłady</a></h2>
<p>W celu <em>ekstrakcji</em> ramek postanowiłem napisać skrypt… w Pythonie. Bo jak się już&nbsp;bawić, to na całego!</p>
<h3 id="sprytny-plan"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#sprytny-plan">Sprytny plan</a></h3>
<p>Plan był dość prosty:</p>
<ol>
<li>Ściągnąć repozytorium Cosmic UI.</li>
<li>Przy pomocy skryptu wyszukać wszystkie pliki komponentów.</li>
<li>Znaleźć w nich ścieżki.</li>
<li>Zmielić ścieżki do <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/path" rel="noreferrer noopener">elementów <code>path</code></a>.</li>
<li>Wygenerować <a href="https://css-tricks.com/svg-symbol-good-choice-icons/" rel="noreferrer noopener">sprite’a SVG</a> i zapisać go do pliku.</li>
</ol>
<p>Stwierdziłem, że repozytorium ściągnę ręcznie, zatem skrypt musiał poprawnie wykonać kroki 2-5. Nie zostało nic innego, jak go napisać.</p>
<h3 id="wyszukiwanie-plików-komponentów"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#wyszukiwanie-plików-komponentów">Wyszukiwanie plików komponentów</a></h3>
<p>Krótki rzut oka na repozytorium Cosmic UI powiedział mi, że pliki z poszczególnymi komponentami są ulokowane w katalogu <code>src/components/ui</code>. Wystarczyło zatem, żeby skrypt do niego wszedł i pobrał listę wszystkich plików <code>.tsx</code>. Ale żeby było ładniej, zadecydowałem przy tym, że ścieżka do katalogu ze ściągniętym repozytorium będzie przekazywana jako argument do pythonowego skryptu.</p>
<p>W Pythonie istnieje klasa <a href="https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser" rel="noreferrer noopener"><code>ArgumentParser</code> w module <code>argparse</code></a>. Pozwala ona tworzyć proste aplikacje CLI, które przyjmują argumenty. Wykorzystajmy ją do obsługi przekazywania ścieżki:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> argparse </span><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ArgumentParser </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse_arguments</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(): </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	arg_parser </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ArgumentParser( </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">		prog</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'Extract SVGs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">		description</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'Extract SVGs from Cosmic UI'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	arg_parser.add_argument( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'dir'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	args </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> arg_parser.parse_args() </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> args </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(): </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	args </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_arguments() </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">main() </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 11</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy klasę z odpowiedniego modułu (1). Następnie tworzymy funkcję <code>parse_arguments()</code> (2). W niej tworzymy nową instancję <code>ArgumentParser</code>a (3). Jako argumenty przekazujemy do konstruktora nazwę naszego “programu” (4) oraz jego krótki opis (5). Następnie dodajemy nowy argument, <code>dir</code> (6). Na samym końcu parsujemy przekazane przez terminal parametry do zmiennej <code>args</code> (7) i je zwracamy (8). Żeby było ładniej, stworzymy przy okazji funkcję <code>main()</code> (9), która będzie opakowaniem na całą logikę&nbsp;aplikacji. Na razie zawiera jedynie parsowanie argumentów (10). Na samym końcu wywołujemy <code>main()</code> (11).</p>
<p>Jeśli zapiszemy teraz nasz skrypt i wywołamy go w terminalu z flagą <code>--help</code>, zauważymy, że wyświetla się nazwa oraz opis przekazane do konstruktora <code>ArgumentParser</code> wraz z krótką instrukcją obsługi:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>$ python ./extract-svgs.py --help</span></span>
<span class="line"><span></span></span>
<span class="line"><span>usage: Extract SVGs [-h] dir</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Extract SVGs from Cosmic UI</span></span>
<span class="line"><span></span></span>
<span class="line"><span>positional arguments:</span></span>
<span class="line"><span>  dir</span></span>
<span class="line"><span></span></span>
<span class="line"><span>options:</span></span>
<span class="line"><span>  -h, --help  show this help message and exit</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Ok, skoro wiemy już, jak przekazać ścieżkę do katalogu z Cosmic UI, pora wyciągnąć przy jej pomocy ścieżki do plików. W tym celu stworzymy funkcję <code>get_component_paths()</code>, która jako argument przyjmuje ścieżkę do katalogu z Cosmic UI:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> os.path </span><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> abspath, join </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> join_path, isfile </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> get_component_paths</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( dir_path: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	cosmic_ui_path </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> abspath( dir_path ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_dir_path </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> abspath( join_path( cosmic_ui_path, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'src/components/ui'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> list</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		filter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			lambda</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path: isfile( path ), </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				lambda</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path: abspath( join_path( components_dir_path, path ) ), </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				listdir( component_dir_path ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_paths.append( abspath( join_path( cosmic_ui_path, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'src/pages/home.tsx'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 11</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> component_paths </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 12</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">():</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	args </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_arguments()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_component_paths( args.dir ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 13</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początek importujemy potrzebne funkcje z modułu <code>os.path</code> (1). Następnie definiujemy funkcję <code>get_component_paths()</code> (2), która przyjmuje ścieżkę do katalogu Cosmic UI i zwraca listę ścieżek do plików z komponentami. Przy pomocy <a href="https://docs.python.org/3/library/os.path.html#os.path.abspath" rel="noreferrer noopener">funkcji <code>abspath()</code></a>  konwertujemy ścieżkę do Cosmic UI na ścieżkę absolutną (3). To zabezpieczenie na wypadek, gdyby ktoś wywołał skrypt ze ścieżką względną (<code>python extract-svgs.py ./cosmic-ui</code>). Następnie <a href="https://docs.python.org/3/library/os.path.html#os.path.join" rel="noreferrer noopener">dołączamy</a> do tej ścieżki ścieżkę do katalogu <code>src/components/ui</code> i konwertujemy&nbsp;całość na ścieżkę&nbsp;absolutną&nbsp;(4). Żeby wyciągnąć listę samych komponentów, tworzymy nową&nbsp;listę <code>component_paths</code> (5), która jest wynikiem <a href="https://docs.python.org/3/library/functions.html#filter" rel="noreferrer noopener">przefiltrowania</a> (6) <a href="https://docs.python.org/3/library/os.path.html#os.path.isfile" rel="noreferrer noopener">funkcją <code>isfile()</code></a> (7) wszystkich absolutnych ścieżek do komponentów. Ścieżki te uzyskamy poprzez <a href="https://docs.python.org/3/library/functions.html#map" rel="noreferrer noopener">zmapowanie</a> (8) wyniku funkcji <code>listdir()</code> na katalogu komponentów – a więc listy wszystkich elementów wewnątrz tego katalogu – (9) na listę ścieżek absolutnych, uzyskanych poprzez dołączenie ścieżki do elementu do ścieżki katalogu komponentów i wrzucenie tego do <code>abspath()</code> (10). W obydwu przypadkach korzystamy z <a href="https://docs.python.org/3/glossary.html#term-lambda" rel="noreferrer noopener">lambdy</a> – odpowiednika funkcji strzałkowych w JS-ie. W składni JS-owej wyglądałoby to następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> componentPaths</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> listDir</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( componentDirPath )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">element</span><span style="color:#D73A49;--shiki-dark:#F97583"> =&gt;</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> absPath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">joinPath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( componentDirPath, element ) ) )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">element</span><span style="color:#D73A49;--shiki-dark:#F97583"> =&gt;</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( element ) )</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Osobiście uważam, że zdecydowanie czytelniej niż w Pythonie.</p>
<p>Do tak stworzonej listy <a href="https://docs.python.org/3/library/stdtypes.html#list.append" rel="noreferrer noopener">dorzucamy</a> jeszcze plik <code>src/pages/home.tsx</code> (11). Robimy to, ponieważ strona główna Cosmic UI ma kilka ciekawych kształtów ramek, które nie są użyte w żadnym komponencie. Następnie zwracamy tak stworzoną listę ścieżek (12) i dorzucamy wywołanie funkcji <code>get_component_paths()</code> do funkcji <code>main()</code> (13).</p>
<h3 id="znajdowanie-ścieżek"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#znajdowanie-ścieżek">Znajdowanie ścieżek</a></h3>
<p>Pora na trudniejszą część&nbsp;zadania: znalezienie ścieżek SVG w plikach komponentów. Moją pierwszą myślą było zaprzęgnięcie do tego jakiegoś parsera TS-a z prawdziwego zdarzenia. Ale szybko odrzuciłem ten pomysł. Każde wystąpienie ścieżki SVG trzymało się bowiem takiego samego schematu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Frame</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	paths</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		'tutaj ścieżki'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	)}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">/&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>A to oznaczało, że można tutaj zastosować wyrażenia regularne! Ułóżmy zatem <a href="https://regex101.com/r/ORcE4G/1" rel="noreferrer noopener">odpowiednie wyrażenie regularne</a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>paths={JSON.parse\(\s*'(?P&lt;path&gt;[^']+)'</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Rozbijmy je na części:</p>
<ol>
<li><code>paths={JSON.parse\(</code> szuka dokładnie takiego fragmentu w kodzie; <code>\</code> (znak ucieczki) przed <code>(</code>  ma związek z tym, że w wyrażeniach regularnych <code>()</code> służą do oznaczania grup;</li>
<li><code>\s*</code> jest “na wszelki wypadek” i oznacza “w tym miejscu może wystąpić&nbsp;dowolna liczba białych znaków lub nie być żadnego”;</li>
<li><code>'(?P&lt;path&gt;[^']+)'</code> tworzy grupę o nazwie <code>path</code>, w której znajduje się co najmniej jeden znak inny niż <code>'</code>; grupa ta jest otoczona <code>'</code>.</li>
</ol>
<p>To wyrażenie powinno wyszukać wszystkie ścieżki SVG w kodzie komponentów. Dodajmy je zatem do kodu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> re </span><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> finditer </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">SVG_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'paths={JSON.parse</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">s*</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\'</span><span style="color:#032F62;--shiki-dark:#9ECBFF">(?P&lt;path&gt;[^</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\'</span><span style="color:#032F62;--shiki-dark:#9ECBFF">]+)</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\'</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse_component</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( component_path: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbols: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_content </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> open</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( component_path, </span><span style="color:#E36209;--shiki-dark:#FFAB70">mode</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'r'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).read() </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	svg_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> finditer( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">SVG_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, component_content ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> svg_paths: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 11</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		symbols.append( path.group( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 12</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> symbols </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 13</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse_components</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( component_paths: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] ) -&gt; list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbols: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> component_path </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> component_paths: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		symbols </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> symbols </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_component( component_path ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> symbols; </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">():</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	args </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_arguments()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_component_paths( args.dir )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbols </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_components( component_paths ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 14</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	print</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( symbols )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">main()</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na sam początek importujemy <a href="https://docs.python.org/3/library/re.html#re.finditer" rel="noreferrer noopener">funkcję <code>finditer()</code> z modułu <code>re</code></a> (1), która pozwala na wyszukanie wszystkich dopasowań do wyrażenia regularnego w jakimś ciągu tekstowym. Następnie tworzymy zmienną <code>SVG_REGEX</code> (2), w której umieszczamy nasze wyrażenie regularne. Z racji tego, że jest ono umieszczone w kodzie jako zwykły ciąg tekstowy, znaki ucieczki są podwójne (zatem <code>\\s</code> zamiast <code>\s</code> itd.) – inaczej traktowane byłyby jako znaki ucieczki w ciągu tekstowym. Następnie tworzymy funkcję&nbsp;<code>parse_components()</code> (3), która przyjmuje listę ścieżek do plików komponentów i zwraca listę wyciągniętych z nich i sparsowanych ścieżek. Sparsowane ścieżki trzymamy w liście <code>symbols</code> (4). Docelowo ma trzymać <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/symbol" rel="noreferrer noopener">elementy <code>symbol</code></a> , stąd nazwa. Następnie dla każdego pliku komponentu (5) wywołujemy funkcję&nbsp;<code>parse_component()</code> (6). Z uwagi na to, że <code>parse_component()</code> może zwrócić listę ścieżek (bo w pliku może być więcej niż jedna ścieżka), stosujemy tutaj łączenie list (<code>nowa_lista = lista1 + lista2</code> – tu z kolei Python jest zdecydowanie elegantszy niż JS!). Na końcu zwracamy listę symboli.</p>
<p>Funkcja <code>parse_component()</code> (7) tworzy swoją własną listę <code>symbols</code> (8), a następnie otwiera plik komponentu i czyta jego treść (9), by potem wyszukać&nbsp;w niej ścieżek przy pomocy wyrażenia regularnego <code>SVG_REGEX</code> (10). Z każdej znalezionej ścieżki (11) wyciąga jedynie wartość grupy <code>path</code> i wrzuca ją do tablicy <code>symbols</code> (12), którą następnie zwraca (13). Na sam koniec dorzucamy wywołanie <code>parse_components()</code> do funkcji <code>main()</code> (14) i dorzucamy <code>print()</code> (15), żeby zobaczyć, czy coś się nie skrzaczyło. Naszym oczom powinna się ukazać… ściana liter i cyfr. Co na tym etapie oznacza tyle, że nasz skrypt faktycznie <em>coś</em> z komponentów wyciąga. Teraz pora to sparsować do sensownej postaci!</p>
<h3 id="mielenie-ścieżek"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#mielenie-ścieżek">Mielenie ścieżek</a></h3>
<p>Przyjrzyjmy się, co tak naprawdę&nbsp;siedzi w każdym komponencie <code>Frame</code>. Jego magiczna własność&nbsp;<code>paths</code> zawiera JSON-a mniej więcej o takim kształcie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"show"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"style"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"style"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"css"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"path"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		[ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"M"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"17"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Nie odkryłem, co robi <code>show</code>, ale <code>style</code> i <code>path</code> były proste do odgadnięcia. Własność <code>style</code> zawiera po prostu style CSS dla danej ścieżki, natomiast <code>path</code> – tzw. <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/d#path_commands" rel="noreferrer noopener">komendy ścieżki</a>. Krótka analiza artykułu na MDN zasugerowała mi, że powyższy kod JSON jest odpowiednikiem mniej więcej takiego SVG:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">XML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> xmlns</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"http://www.w3.org/2000/svg"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">path</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> d</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"M 17,0"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"style: css;"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Skoro tak, to sparsowanie tego do używalnej formy nie powinno stanowić&nbsp;większego problemu! Przystąpmy zatem do pracy. Na początek stwórzmy funkcję <code>create_symbol()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> json </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">SYMBOL_WIDTH</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 14</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">SYMBOL_HEIGHT</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 15</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">current_symbol </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 11</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> create_symbol</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( svg_string: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	global</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> current_symbol</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	paths: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	path_data </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> json.loads( svg_string ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path_data: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		path_coords </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> create_path_commands( path[ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		path_style </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> create_path_style( path[ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'style'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		paths.append( </span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'&lt;path d="</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">path_coords</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">" style="</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">path_style</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/&gt;'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbol </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'&lt;symbol id="cosmic-</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">current_symbol</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">" viewBox="0 0 </span><span style="color:#005CC5;--shiki-dark:#79B8FF">{SYMBOL_WIDTH}</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> {SYMBOL_HEIGHT}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"&gt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.join( paths )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;/symbol&gt;'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 9</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	current_symbol </span><span style="color:#D73A49;--shiki-dark:#F97583">+=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 13</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> symbol </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początek importujemy <a href="https://docs.python.org/3/library/json.html#module-json" rel="noreferrer noopener">moduł <code>json</code></a> (1), odpowiedzialny za pracę z JSON-em. Następnie definiujemy funkcję <code>create_symbol()</code> (2), która będzie generować elementy <code>symbol</code> z poprawnymi ścieżkami. Wewnątrz niej tworzymy zmienną <code>paths</code> (3), która będzie przechowywać stworzone elementy <code>path</code>. Następnie do zmiennej <code>path_data</code> parsujemy przy pomocy <a href="https://docs.python.org/3/library/json.html#json.loads" rel="noreferrer noopener">funkcji <code>json.loads()</code></a> wyciągnięte przez nas ścieżki z komponentów (4). Dla każdej wyciągniętej ścieżki (5) tworzymy komendy ścieżki  przy pomocy funkcji <code>create_path_commands()</code> (6) oraz style przy pomocy funkcji <code>create_path_style()</code> (7), a następnie na tej podstawie generujemy element <code>path</code> (przy pomocy <a href="https://docs.python.org/3/glossary.html#term-f-string" rel="noreferrer noopener">f-stringu</a>) i wsadzamy go do listy <code>paths</code> (8). Na samym końcu tworzymy z tego element <code>symbol</code> (9) i go zwracamy (10). System sprite’ów wymaga, żeby każdy symbol miał swoje id, więc tworzymy go według wzoru <code>cosmic-&lt;kolejna liczba&gt;</code>. Licznik trzymany w globalnej zmiennej <code>current_symbol</code> (11). Wewnątrz funkcji <code>create_symbol()</code> zaznaczamy jej użycie słówkiem kluczowym <code>global</code> (12), następnie wykorzystujemy przy tworzeniu elementu <code>symbol</code> (9), a potem – zwiększamy o 1 jej wartość (13). Dodatkowo każdy symbol ma określony <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/viewBox" rel="noreferrer noopener">viewbox</a> przy pomocy zmiennych <code>SYMBOL_WIDTH</code> (14) i <code>SYMBOL_HEIGHT</code> (15) – w naszym przypadku obydwie te zmienne mają&nbsp;wartość&nbsp;<code>100</code>.</p>
<p>Funkcję <code>create_symbol()</code> wsadzamy teraz do <code>parse_component()</code> (1):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse_component</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( component_path: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbols: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_content </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> open</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( component_path, </span><span style="color:#E36209;--shiki-dark:#FFAB70">mode</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'r'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).read()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	svg_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> finditer( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">SVG_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, component_content )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> svg_paths:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		symbols.append( create_symbol( path.group( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> symbols</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Przyjrzyjmy się jeszcze funkcjom generującym style i komendy ścieżki. Na początek <code>create_path_style()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> create_path_style</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( style ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	style_string_parts: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> property</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, value </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> style.items(): </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		style_string_parts.append( </span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{property}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">value</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ';'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.join( style_string_parts ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy listę <code>style_string_parts</code>, następnie dla <a href="https://docs.python.org/3/library/stdtypes.html#dict.items" rel="noreferrer noopener">każdej pary klucz → wartość ze słownika</a> <code>style</code> (2) tworzymy nową&nbsp;regułę CSS (3). Na samym końcu <a href="https://docs.python.org/3/library/stdtypes.html#str.join" rel="noreferrer noopener">łączymy</a> wszystkie reguły CSS w jeden ciąg tekstowy, odgradzając poszczególne z nich średnikami (4).</p>
<p>Natomiast funkcja <code>create_path_commands()</code> prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> create_path_commands</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( path ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	commands: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		command_name </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		x </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		y </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		commands.append( </span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">command_name</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> {</span><span style="color:#24292E;--shiki-dark:#E1E4E8">x</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">,</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">y</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ' '</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.join( commands ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy listę <code>commands</code> (1). Następnie z każdej komendy zapisanej w JSON-ie (2) wyciągamy jej nazwę, czyli pierwszy element listy (3), współrzędną na osi X, zatem drugi element listy (4), oraz współrzędną na osi Y, czyli trzeci element listy (5). Formatujemy to jako poprawną komendę ścieżki i wrzucamy do listy <code>commands</code> (6). Na końcu robimy z komend jeden ciąg tekstowy i go zwracamy (7).</p>
<div class="note" role="note" aria-labelledby="note-20"><p class="note__label" id="note-20">Dygresja</p><div class="note__content"><p>W kodzie za każdym razem pobieramy pierwszy element listy, ponieważ <code>pop()</code> usuwa element listy i go zwraca. Tym samym po usunięciu pierwszego elementu ten drugi staje się pierwszy itd.</p></div></div>
<p>Jeśli teraz uruchomimy nasz skrypt, dostaniemy gotowe do użycia elementy <code>symbol</code>! Dla testów możemy skopiować dowolny, opatulić go w <code>svg</code> i spróbować użyć:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- tutaj dowolny symbol --&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">use</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"#symbol-1"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Gdy otworzymy taki plik HTML, to okaże się, że… nie działa. A po otwarciu konsoli w Chrome wita nas piękny komunikat błędu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>Error: &lt;path&gt; attribute d: Expected number, "M 14,6 L 50% - 7,6 L 50% - …".</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Spojrzałem zatem dokładniej w kod Cosmic UI. I wówczas odkryłem, że w JSON-ie nie do końca są ścieżki. Były tam też… działania matematyczne, takie jak <code>100% - 40</code>. Pogrzebałem trochę głębiej i odkryłem, że faktyczne wartości dla ścieżek są <a href="https://github.com/rizkimuhammada/cosmic-ui/blob/fd677955959290be06438c1656b8141391951deb/src/utils/frame.ts#L18-L41" rel="noreferrer noopener">obliczane na bieżąco</a>, na podstawie wielkości poszczególnych komponentów. To był ten moment, w którym chciałem porzucić cały projekt, ale <a href="https://en.wikipedia.org/wiki/Sunk_cost#Fallacy_effect" rel="noreferrer noopener">zainwestowałem już za dużo czasu</a>. Nie zostało mi nic innego, jak dodać obliczenia do mojego skryptu.</p>
<figure class="figure"><a class="figure__link" href="/assets/images/kosmiczna-zabawa/kontemplacja-660w.avif"><picture><source type="image/avif" srcset="/assets/images/kosmiczna-zabawa/kontemplacja-440w.avif 440w, /assets/images/kosmiczna-zabawa/kontemplacja-660w.avif 660w" sizes="90vw"><source type="image/webp" srcset="/assets/images/kosmiczna-zabawa/kontemplacja-440w.webp 440w, /assets/images/kosmiczna-zabawa/kontemplacja-660w.webp 660w" sizes="90vw"><source type="image/png" srcset="/assets/images/kosmiczna-zabawa/kontemplacja-440w.png 440w, /assets/images/kosmiczna-zabawa/kontemplacja-660w.png 660w" sizes="90vw"><img src="/assets/images/kosmiczna-zabawa/kontemplacja-660w.png" width="660" height="825" style="aspect-ratio: 660 / 825" alt="Fotomontaż: Ben Affleck z moją twarzą, stojący oparty o drzwi i palący papierosa." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Ja, kontemplujący swoje złe decyzje życiowe</p></figcaption></figure>
<p>Na szczęście nie okazało się to jakoś przesadnie trudne. Na sam początek trzeba zmienić funkcję <code>create_path_commands()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> create_path_commands</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( path ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	commands: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data </span><span style="color:#D73A49;--shiki-dark:#F97583">in</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		command_name </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		x </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> calculate_coord( command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ), </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'x'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		y </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> calculate_coord( command_data.pop( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ), </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'y'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		commands.append( </span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">command_name</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> {</span><span style="color:#24292E;--shiki-dark:#E1E4E8">x</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">,</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">y</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ' '</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.join( commands )</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Teraz zmienne <code>x</code> i <code>y</code> tworzymy przez wywołanie funkcji <code>calculate_coord()</code>, do której przekazujemy odpowiednią wartość z listy <code>command_data</code> oraz oś, do której koordynat należy (1, 2).</p>
<p>Sama funkcja <code>calculate_coord()</code> przedstawia się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> re </span><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> finditer, search </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">CALC_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '^(?P&lt;left&gt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">d+(?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">d+)?%?)</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">s*(?P&lt;operator&gt;[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">-+*/])</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">s*(?P&lt;right&gt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">d+(?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">d+)?%?)$'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> calculate_coord</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( coord: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, axis: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> coord.find( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'%'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">==</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> coord</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	calculation </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> search( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">CALC_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, coord ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> calculation </span><span style="color:#D73A49;--shiki-dark:#F97583">==</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> None</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> coord; </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	left </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_coord_value( calculation.group( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'left'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ), axis ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	operator </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> calculation.group( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'operator'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	right </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_coord_value( calculation.group( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'right'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ), axis ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 9</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	match</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> operator: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		case</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '-'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{int</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( left </span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> right )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 11</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		case</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '+'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{int</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( left </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> right )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 12</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		case</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '*'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{int</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( left </span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> right )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 13</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		case</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{int</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( left </span><span style="color:#D73A49;--shiki-dark:#F97583">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> right )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 14</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na sam początek sprawdzamy, czy koordynat <a href="https://docs.python.org/3/library/stdtypes.html#str.find" rel="noreferrer noopener">zawiera znak</a> <code>%</code> (1). Jeśli nie, zwracamy przekazany koordynat bez zmian (Cosmic UI <em>zawsze</em> stosuje procenty w obliczeniach, stąd ten warunek). Następnie przy pomocy funkcji <code>search()</code> z modułu <code>re</code> (2) wyszukujemy dopasowanie (3) do wyrażenia regularnego <code>CALC_REGEX</code> (4) wewnątrz przekazanego koordynatu. Jeśli go nie znajdujemy (5), zwracamy koordynat bez zmian (6). W innym wypadku wyciągamy z dopasowania lewą&nbsp;stronę działania (7), operator (8) oraz prawą stronę działania (9). Następnie <a href="https://docs.python.org/3/reference/compound_stmts.html#index-18" rel="noreferrer noopener">sprawdzamy, jaki mamy operator</a> (10) i w zależności od tego, wykonujemy odpowiednie działanie – odejmowanie (11), dodawanie (12), mnożenie (13) lub dzielenie (14).</p>
<p>Przyjrzyjmy się jeszcze wyrażeniu regularnemu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>^(?P&lt;left&gt;\d+(?:\.\d+)?%?)\s*(?P&lt;operator&gt;[\-+*/])\s*(?P&lt;right&gt;\d+(?:\.\d+)?%?)$</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><ol>
<li><code>^</code> oznacza, że dopasowanie musi się&nbsp;zaczynać od początku ciągu;</li>
<li><code>(?P&lt;left&gt;\d+(?:\.\d+)?%?)</code> to grupa oznaczająca lewą stronę działania:
<ol>
<li><code>?P&lt;left&gt;</code> nadaje nazwę <code>left</code> grupie,</li>
<li><code>\d+</code> oznacza “co najmniej jedną cyfrę”,</li>
<li><code>(?:\.\d+)?</code> oznacza “w tym miejscu może wystąpić kropka, po której następuje co najmniej jedna cyfra” (czyli liczby po przecinku),</li>
<li><code>%?</code> oznacza “w tym miejscu może wystąpić znak procenta”;</li>
</ol>
</li>
<li><code>\s*(?P&lt;operator&gt;[\-+*/])\s*</code> to operator (znak <code>-</code>, <code>+</code>, <code>*</code> lub <code>/</code>), który może być otoczony z obydwu stron białymi znakami;</li>
<li><code>(?P&lt;right&gt;\d+(?:\.\d+)?%?)</code> to grupa oznaczająca prawą&nbsp;stronę działania; jest identyczna, jak dla lewej strony;</li>
<li><code>$</code> oznacza, że dopasowanie musi się kończyć na końcu ciągu; w połączeniu z <code>^</code> sprawia, że cały ciąg musi być dopasowany.</li>
</ol>
<p>Natomiast funkcja <code>get_coord_value()</code> odpowiednio konwertuje każdą stronę działania:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> get_coord_value</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( raw_value: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, axis: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">float</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> raw_value.find( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'%'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">==</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> float</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( raw_value ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	raw_number </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> float</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( raw_value.replace( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'%'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> float</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( raw_number </span><span style="color:#D73A49;--shiki-dark:#F97583">/</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">SYMBOL_WIDTH</span><span style="color:#D73A49;--shiki-dark:#F97583"> if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> axis </span><span style="color:#D73A49;--shiki-dark:#F97583">==</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'x'</span><span style="color:#D73A49;--shiki-dark:#F97583"> else</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> SYMBOL_HEIGHT</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli przekazany ciąg nie zawiera znaku <code>%</code> (1), wówczas konwertujemy go na liczbę zmiennoprzecinkową i zwracamy (2). W  innym wypadku usuwamy z liczby znak procenta i konwertujemy ją na liczbę zmiennoprzecinkową (3). Następnie konwertujemy procenty na odpowiednią wartość (4):</p>
<ol>
<li>dzielimy <code>raw_number</code> przez 100,</li>
<li>mnożymy to przez szerokość symbolu, jeśli to koordynat dla osi X, lub przez wysokość symbolu, jeśli to koordynat dla osi Y.</li>
</ol>
<p>Tak przekonwertowaną wartość zwracamy.</p>
<p>Jeśli teraz przetestujemy nasz skrypt, to otrzymamy działające symbole 🎉! Tylko że nie do końca…</p>
<figure class="figure"><a class="figure__link" href="/assets/images/kosmiczna-zabawa/komponent-problem-677w.avif"><picture><source type="image/avif" srcset="/assets/images/kosmiczna-zabawa/komponent-problem-440w.avif 440w, /assets/images/kosmiczna-zabawa/komponent-problem-677w.avif 677w" sizes="90vw"><source type="image/webp" srcset="/assets/images/kosmiczna-zabawa/komponent-problem-440w.webp 440w, /assets/images/kosmiczna-zabawa/komponent-problem-677w.webp 677w" sizes="90vw"><source type="image/png" srcset="/assets/images/kosmiczna-zabawa/komponent-problem-440w.png 440w, /assets/images/kosmiczna-zabawa/komponent-problem-677w.png 677w" sizes="90vw"><img src="/assets/images/kosmiczna-zabawa/komponent-problem-677w.png" width="677" height="908" style="aspect-ratio: 677 / 908" alt="Zniekształcone, nachodzące na siebie linie, tworzące bliżej nieokreślone kształty." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Comandeer, Sztuka abstrakcyjna, 2025, kodem na monitorze</p></figcaption></figure>
<p>Po raz kolejny zagłębiłem się w kod Cosmic UI, szukając przyczyny takiego zachowania. Intuicja podpowiadała, że coś jest nie tak z obliczeniami – ale empiryczne sprawdzenie ich dla kilku losowych komponentów pokazywało, że wszystko jest liczone poprawnie. W końcu zauważyłem pewną rzecz w komponencie <code>dialog</code>. A dokładniej dwie ścieżki obok siebie w JSON-ie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"L"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"100% - 7"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"100% - 33.33333333333332%"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"L"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"100% - 7"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"100% - 40"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wówczas doszło do mnie, na czym polega problem. Mój skrypt jak najbardziej liczył poprawnie! Problem polegał na tym, że ustawiłem wysokość i szerokość symboli na 100. Przy tych wartościach pierwsza ścieżka na osi Y miała koordynat ok. 66.7, natomiast druga – 60. Niemniej żaden komponent Cosmic UI nigdy nie miał takich małych rozmiarów! Jeśli przyjmiemy, że najmniejszy rozmiar to 150×150 (a w rzeczywistości praktycznie zawsze był większy), to wówczas drugi z tych koordynatów będzie <em>zawsze</em> większy. Zmieniłem zatem wielkość symbolu na 640×480 i… zaczęło działać.</p>
<figure class="figure"><a class="figure__link" href="/assets/images/kosmiczna-zabawa/komponent-final-1024w.avif"><picture><source type="image/avif" srcset="/assets/images/kosmiczna-zabawa/komponent-final-440w.avif 440w, /assets/images/kosmiczna-zabawa/komponent-final-880w.avif 880w, /assets/images/kosmiczna-zabawa/komponent-final-1024w.avif 1024w" sizes="90vw"><source type="image/webp" srcset="/assets/images/kosmiczna-zabawa/komponent-final-440w.webp 440w, /assets/images/kosmiczna-zabawa/komponent-final-880w.webp 880w, /assets/images/kosmiczna-zabawa/komponent-final-1024w.webp 1024w" sizes="90vw"><source type="image/png" srcset="/assets/images/kosmiczna-zabawa/komponent-final-440w.png 440w, /assets/images/kosmiczna-zabawa/komponent-final-880w.png 880w, /assets/images/kosmiczna-zabawa/komponent-final-1024w.png 1024w" sizes="90vw"><img src="/assets/images/kosmiczna-zabawa/komponent-final-1024w.png" width="1024" height="763" style="aspect-ratio: 1024 / 763" alt="Poprawnie wygenerowany komponent, przypominający kształtem i kolorem element interfejsu komputera z powieści sci-fi." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Comandeer, Komponent sci-fi, 2025, kodem na monitorze</p></figcaption></figure>
<h3 id="zapisanie-svg"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#zapisanie-svg">Zapisanie SVG</a></h3>
<p>Została zatem ostatnia część&nbsp;do zrobienia: zapisanie tego w postaci sprite’a SVG.</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Python</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> create_svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( symbols: list[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">] ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#D73A49;--shiki-dark:#F97583"> f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'&lt;svg xmlns="http://www.w3.org/2000/svg"&gt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.join( symbols )</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;/svg&gt;'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> save_svg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( svg_content: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) -&gt; </span><span style="color:#005CC5;--shiki-dark:#79B8FF">None</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	svg_path </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> abspath( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'./cosmic.svg'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	with</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> open</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( svg_path, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'w'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">encoding</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'utf-8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#E36209;--shiki-dark:#FFAB70"> file</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">		file</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.write( svg_content ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">():</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	args </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_arguments()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	component_paths </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_component_paths( args.dir )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	symbols </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> parse_components( component_paths )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	svg_content </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> create_svg( symbols ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	save_svg( svg_content ) </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy dwie nowe funkcje – <code>create_svg()</code> (1), która przyjmuje listę wygenerowanych symboli i zwraca SVG jako ciąg tekstowy, oraz <code>save_svg()</code> (2), która przyjmuje ten ciąg i zapisuje go do pliku. Funkcja <code>create_svg()</code> łączy wszystkie symbole w jeden ciąg tekstowy, a następnie wkłada go do środka znacznika <code>svg</code> i zwraca tak stworzony ciąg (3). Z kolei funkcja <code>save_svg()</code> otwiera plik SVG (4) pod ścieżką&nbsp;<code>./cosmic.svg</code> (5) i zapisuje do niego ciąg tekstowy z kodem SVG (6). Na sam koniec dorzucamy obydwie funkcje do funkcji <code>main()</code> (7, 8).</p>
<p>Tym sposobem udało nam się&nbsp;wyciągnąć wszystkie kształty z Cosmic UI i wygenerować sprite’a SVG. Możemy być z siebie dumni! <a href="https://gist.github.com/Comandeer/1c5ce5d7ca2143ba17b64c635f7f57e8" rel="noreferrer noopener">Całość skryptu</a> (z lekkimi zmianami względem tego postu) jest na Giście.</p>
<h2 id="smutny-koniec"><a class="header-anchor" href="https://blog.comandeer.pl/kosmiczna-zabawa#smutny-koniec">Smutny koniec</a></h2>
<p>Gdy już się udało to wszystko zrobić, <a href="https://www.youtube.com/watch?v=-nOkR7HjyO4" rel="noreferrer noopener">doszło do mnie, że to bez sensu</a>… Bo z uwagi na to, jak są generowane te ramki (jako jeden duży kształt w SVG), nie są one responsywne. A to wyklucza je z większości zastosowań, jakie mógłbym dla nich mieć.</p>
<p>Zrobienie tak stylizowanego interfejsu w sposób w pełni responsywny jest trudne. Nie wiem, jakbym podszedł do takiego problemu. To, co mi chodzi po głowie, to podzielenie tych ramek na części, np.:</p>
<ol>
<li>część&nbsp;dla lewego górnego rogu,</li>
<li>część&nbsp;dla prawego górnego rogu,</li>
<li>część&nbsp;dla lewego dolnego rogu,</li>
<li>część dla prawego dolnego rogu</li>
<li>powtarzalne obramowanie dla góry,</li>
<li>powtarzalne obramowanie dla dołu,</li>
<li>powtarzalne obramowanie dla lewej strony,</li>
<li>powtarzalne obramowanie dla prawej strony,</li>
<li>powtarzalny wzór tła dla środka kontenera z treścią.</li>
</ol>
<p>Dzięki powtarzalnym obramowaniom i tłu możliwe byłoby rozciąganie faktycznej treści teoretycznie w nieskończoność – zarówno w poziomie, jak i pionie. Jedynie same rogi pozostawały zawsze takie same.</p>
<p>Ale Cosmic UI, niestety, nie oferuje takiego rozwiązania. No cóż, na osłodę łez zostaje przynajmniej <a href="https://fonts.google.com/specimen/Orbitron" rel="noreferrer noopener">font Orbitron</a>, który w Cosmic UI dopełniał całości iluzji, a którego użycie raczej będzie mnie kosztowało zdecydowanie mniej pracy.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Kreda, czyli reakcja łańcuchowa</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/kreda-czyli-reakcja-lancuchowa" rel="alternate" type="text/html"/>
			<published>2024-04-30T22:25:00.000Z</published>
			<updated>2024-04-30T22:25:00.000Z</updated>
			<id>https://blog.comandeer.pl/kreda-czyli-reakcja-lancuchowa</id>
			
				<summary><![CDATA[Jak odtworzyć API Chalka przy użyciu proxy?]]></summary>
			
			<content type="html"><![CDATA[<p>Node.js w wersji 21.7.0 dodał <a href="https://nodejs.org/docs/latest-v21.x/api/util.html#utilstyletextformat-text" rel="noreferrer noopener">natywne wsparcie dla kolorków w terminalu</a>, yay! Teraz można łatwo i przyjemnie stylować tekst w terminalu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { styleText } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:util'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( util.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">styleText</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'underline'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, util.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">styleText</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'italic'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Podkreślony, pochylony tekst'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) ),</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Hmm, powiedziałem <em>łatwo i przyjemnie</em>… A mówiąc to, mam na myśli tak naprawdę to, w jaki sposób zachowuje się <a href="https://www.npmjs.com/package/chalk" rel="noreferrer noopener">Chalk</a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> chalk </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'chalk'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( chalk.underline.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">italic</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Podkreślony, pochylony tekst'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zdecydowanie czytelniej i jakoś tak <em>milej</em>. No więc postanowiłem spróbować dorobić taki interfejs do natywnego wsparcia kolorków.</p>
<h2 id="kreda"><a class="header-anchor" href="https://blog.comandeer.pl/kreda-czyli-reakcja-lancuchowa#kreda">Kreda</a></h2>
<p>Owocem mojego eksperymentu jest pakiet <a href="https://www.npmjs.com/package/kreda" rel="noreferrer noopener"><code>kreda</code></a>. Używa on pod spodem <a href="https://blog.comandeer.pl/uniwersalny-getter.html">moich ulubionych proxy</a>. Czemu zdecydowałem się na ich użycie? Przyjrzyjmy się łańcuszkowi stworzonemu przez Chalk. Widzimy, że mamy tam własność <code>underline</code> oraz metodę <code>italic()</code>. Jak na razie wygląda to jak typowe zastosowanie techniki chainingu (łańcuchowania). Coś typu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Chalk</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">	underline</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	italic</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> chalk</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Chalk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">chalk.underline.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">italic</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Jakiś tekst'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Mamy sobie klasę <code>Chalk</code> (1), która ma własność <code>underline</code> (2) oraz metodę <code>italic()</code> (3). Metoda ta zwraca <code>this</code> (4), podobnie jak własność. Dzięki temu w momencie, gdy stworzymy sobie instancję tej klasy (5), możemy tworzyć łańcuszek (6).</p>
<p>Tylko że pojawia się drobny problem:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// To też działa:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">chalk.italic.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">underline</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Jakiś tekst'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Innymi słowy: w Chalku każda własność&nbsp;może być równocześnie metodą, jeśli zajdzie taka potrzeba. I na odwrót: każda metoda to też własność. Stąd moim pierwszym skojarzeniem były proxy – one wszak pozwalają na dziwną magię w obiektach.</p>
<p>I faktycznie, udało mi się stworzyć prosty mechanizm, który tak się&nbsp;zachowuje!</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> kreda</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createProxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( [] ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createProxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">modifiers</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Proxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {}, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">target</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">property</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createProxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( [ </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">modifiers, property ] ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		apply</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">target</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">thisArg</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">args</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( modifiers, </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">args ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Stworzyłem funkcję <code>createProxy()</code> (1), która jako argument przyjmuje tablicę modyfikatorów (stylów tekstu). Na samym początku jest ona pusta (2). Funkcja <code>createProxy()</code> zwraca <code>Proxy</code> (3), które ma dwie pułapki – <code>get()</code> (4) do obsługi własności i <code>apply()</code> (5) do obsługi wywołania funkcji. Warto zwrócić uwagę na to, ze proxy jest tworzone na pustej funkcji strzałkowej. Inaczej pułapka <code>apply()</code> nie zadziała (dostaniemy błąd, że próbujemy wywołać coś, co nie jest funkcją)</p>
<p>W chwili, gdy następuje odwołanie do dowolnej własności proxy, pułapka <code>get()</code> zwraca nowe proxy (6). Jako argument przekazuje tablicę zawierającą wszystkie obecne modyfikatory + nazwę żądanej własności (np. <code>underline</code>). Z kolei, gdy ktoś chce wywołać własność, odpalana jest funkcja <code>style()</code> (7), która jako 1. argument dostaje tablicę wszystkich modyfikatorów, a jako kolejne – poszczególne argumenty przekazane do wywołania funkcji. Przykładowo:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">kreda.underline.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">italic</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Test'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// oznacza</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">style</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( [ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'underline'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'italic'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ], </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Test'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Sama zaś funkcja <code>style()</code> po prostu wywołuje natywną funkcję Node’a od kolorowania tekstu. Tym sposobem udało się stworzyć chalkowy łańcuszek! Jak takie coś działa, z dodatkową walidacją i kilkoma innymi ficzerami, można zobaczyć w <a href="https://github.com/Comandeer/kreda/blob/main/src/index.ts" rel="noreferrer noopener">kodzie kredy</a>.</p>
<h2 id="a-jak-to-robi-chalk"><a class="header-anchor" href="https://blog.comandeer.pl/kreda-czyli-reakcja-lancuchowa#a-jak-to-robi-chalk">A jak to robi Chalk?</a></h2>
<p><em>Magicznie ✨</em>.</p>
<p>Niemniej z tego, co zrozumiałem, Chalk za każdym razem zwraca funkcję, której prototyp jest modyfikowany “na żywca” tak, aby zawierał poszczególne własności. Mniej więcej coś takiego:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> chainPrototype</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Object.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">defineProperties</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {}, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	property1: {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createChain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	property2: {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createChain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createChain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> chain</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">text</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( text ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	Object.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">setPrototypeOf</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( chain, chainPrototype ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> chain; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> chain</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createChain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">chain.property1.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">property2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Powinienem się wyświetlić w konsoli'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku tworzony jest prototyp dla łańcucha (1). Odbywa się to przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties" rel="noreferrer noopener"><code>Object.defineProperties()</code></a>. Jest ono wykorzystywane z powodu możliwości definiowania w wygodny sposób getterów. Każda własność ma getter (2), który wywołuje funkcję <code>createChain()</code> (3). Funkcja ta tworzy funkcję, która ostatecznie ma zostać wywołana (4). W naszym wypadku wyświetla ona jedynie przekazany tekst w konsoli (5). Następnie do prototypu tej funkcji doczepiany jest nasz prototyp łańcucha (6). Tak zmodyfikowana funkcja jest następnie zwracana (7). Łańcuchowanie zaczyna się od przypisana wyniku <code>createChain()</code> do zmiennej (8). Dzięki temu można dowolnie łączyć ze sobą własności i każdą z nich wywoływać jak funkcję (9).</p>
<p>Czemu Chalk robi to w ten sposób? Jednym z powodów jest zapewne kompatybilność takiego rozwiązania, które powinno działać od Node 0.12.0 (czyli <em>od zawsze</em>). Drugi powód, który przychodzi mi na myśl, to wydajność – nie zdziwiłoby mnie, gdyby rozwiązanie oparte o <code>Proxy</code> było wolniejsze (aczkolwiek nie robiłem benchmarków). Niemniej osobiście wydaje mi się, że chalkowe rozwiązanie ma zdecydowanie wyższy <a href="https://www.osnews.com/story/19266/wtfsm/" rel="noreferrer noopener">wskaźnik WTF/minuta</a>, niźli rozwiązanie kredowe.</p>
<h2 id="wielki-powrót-nodea"><a class="header-anchor" href="https://blog.comandeer.pl/kreda-czyli-reakcja-lancuchowa#wielki-powrót-nodea">Wielki powrót Node’a</a></h2>
<p>Stylowanie tekstu w terminalu zostało przeportowane do <a href="https://nodejs.org/docs/latest-v20.x/api/util.html#utilstyletextformat-text" rel="noreferrer noopener">Node’a 20.12.0</a> praktycznie bez zmian. Ale weszło też do nowego <a href="https://nodejs.org/api/util.html#utilstyletextformat-text" rel="noreferrer noopener">Node’a 22</a>. I tam już poprawiono ergonomię! Teraz można przekazać tablicę formatów zamiast pojedynczego formatu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { styleText } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:util'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( util.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">styleText</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( [ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'underline'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'italic'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ], </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Podkreślony, pochylony tekst'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ),</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zdecydowanie przyjemniej, ale IMO wciąż łańcuszek jest nieco czytelniejszy. Nie jest to jednak aż tak duża różnica, żeby kruszyć o to kopie. Co oznacza, że <code>kreda</code> raczej nie doczeka się jakichś wielkich aktualizacji, ale mimo wszystko była ciekawym eksperymentem.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">W odwiedzinach u Bramkarza</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/w-odwiedzinach-u-bramkarza" rel="alternate" type="text/html"/>
			<published>2023-10-31T18:13:00.000Z</published>
			<updated>2023-10-31T18:13:00.000Z</updated>
			<id>https://blog.comandeer.pl/w-odwiedzinach-u-bramkarza</id>
			
				<summary><![CDATA[Przyjrzenie się zmianom, jakie nastąpiły wokół systemu uprawnień w Node.js.]]></summary>
			
			<content type="html"><![CDATA[<p>Słońce leniwie chowało się za widnokręgiem, rozrzucając pomarańczowe smugi po bezchmurnym, alabastrowym niebie, jakby było wielkim krakenem rozczapierzającym swoje macki. Szedłem złotożółtą plażą kontrastującą z lazurową tonią oceanu, w której odbijały się heroiczne zmagania niebios z wodną bestią. Piasek chrzęścił pod moimi stopami, jakby szepcząco skarżąc się na brutalność mego kroku. Widziałem go – siedział na leżaku, udając, że życiodajna ognista kula wciąż góruje na nieboskłonie, a jej kapryśne promienie tylko czekają, by smagać jego bladą skórę. Gdy stanąłem nad nim i pochyliłem się, miał zamknięte oczy. Drapieżny mrok rzucanego przeze mnie cienia zmusił go do spojrzenia na mnie – spojrzenia jakże zaskoczonego i pytającego. Powolnym, acz zdecydowanym, ruchem dłoni zdjąłem okulary przeciwsłoneczne i, patrząc cynicznie w jego wciąż nierozumiejące oczy, powiedziałem spokojnie ochrypłym głosem: “Wstawaj, Bramkarzu, mamy Node’a do spalenia”.</p>
<h2 id="zmiana-krajobrazu"><a class="header-anchor" href="https://blog.comandeer.pl/w-odwiedzinach-u-bramkarza#zmiana-krajobrazu">Zmiana krajobrazu</a></h2>
<p>Nieco ponad rok temu <a href="https://blog.comandeer.pl/bramkarz.html">stworzyłem Bramkarza</a>, po czym <a href="https://blog.comandeer.pl/bramkarz-na-urlopie.html">szybko odesłałem go na urlop</a>. Od tamtego czasu trochę się pozmieniało w krajobrazie narzędzi do “pilnowania” kodu odpalanego w Node.js. <a href="https://github.com/yaakov123/hagana" rel="noreferrer noopener">Hagana</a>, czyli pierwowzór Bramkarza, w sumie umarła mniej więcej w tym samym czasie i nie dostała żadnej aktualizacji od lipca tamtego roku. Natomiast pojawiło się nowe narzędzie tego typu, <a href="https://www.npmjs.com/package/@sandworm/guard" rel="noreferrer noopener"><code>@sandworm/guard</code></a>… mające <em>jedno</em> pobranie tygodniowo i zaktualizowane 9 miesięcy temu. Innymi słowy: również podzieliło los Bramkarza. Więc pod względem userlandowych rozwiązań za dużo się nie wydarzyło.</p>
<p>Wydarzyło się za to po stronie samego Node’a. Pojawił się bowiem <a href="https://nodejs.org/docs/latest-v20.x/api/permissions.html" rel="noreferrer noopener">eksperymentalny system uprawnień</a>. Dzięki niemu można m.in. blokować operacje na plikach. Gdy uruchomi się Node’a z flagą <code>--experimental-permission</code>, domyślnie wszystkie operacje na plikach będą blokowane:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Shell</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">node</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> --experimental-permission</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">Welcome</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Node.js</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> v20.5.0.</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">Type</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ".help"</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> for</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> more</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> information.</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">&gt;</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">Access</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> FileSystemWrite</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> is</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> restricted.</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> const { </span><span style="color:#6F42C1;--shiki-dark:#B392F0">readFile</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">'node:fs/promises'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">undefined</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> await readFile( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">'./test'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">Uncaught</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Error:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Access</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> this</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> API</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> has</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> been</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> restricted</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    at</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> open</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (node:internal/fs/promises:595:19)</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    at</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (node:internal/fs/promises:1042:20)</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    at</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> REPL2:1:39</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  code:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'ERR_ACCESS_DENIED',</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  permission:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'FileSystemRead',</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  resource:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '/Users/comandeer/test'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Można też m.in. ograniczać URL-e, do których mają mieć dostęp poszczególne moduły. Tym samym Bramkarz jest jeszcze mniej potrzebny.</p>
<h2 id="bindingi"><a class="header-anchor" href="https://blog.comandeer.pl/w-odwiedzinach-u-bramkarza#bindingi">Bindingi</a></h2>
<p>Niemniej przez ten rok udało mi się też rozwiązać inną zagadkę związaną z Bramkarzem. Ubolewałem mocno nad tym, że poprawne zabezpieczenie zarówno modułów CJS, jak i modułów ESM, wymaga tak naprawdę stworzenia dwóch różnych implementacji <a href="https://nodejs.org/docs/latest-v20.x/api/fs.html" rel="noreferrer noopener">wbudowanego modułu <code>fs</code></a>. Dodatkowo wersja dla ESM była mocno upierdliwa. Okazuje się jednak, że byłem w błędzie!</p>
<p>W jednym ze swoich projektów używam <a href="https://www.npmjs.com/package/mock-fs" rel="noreferrer noopener">paczki <code>mock-fs</code></a> do testów. Jej zadaniem jest stworzenie fałszywego systemu plików w pamięci, dzięki czemu ma się pełną kontrolę nad tym, jakie pliki są dostępne dla aplikacji. Myślałem, że paczka ta robi to, co ja w Bramkarzu: żmudnie nadpisuje wszystkie metody modułu <code>fs</code>. Spodziewałem się więc, że nie zadziała, gdy przepiszę mój kod na ESM. Ale przepisałem, a testy dalej działały… Zafrapowało mnie to i odkryłem wówczas <a href="https://www.npmjs.com/package/mock-fs#upgrading-to-version-4" rel="noreferrer noopener">pewien fragment w dokumentacji</a>:</p>
<blockquote>
<p><span lang="en">Instead of overriding all methods of the built-in <code>fs</code> module, the library now overrides <code>process.binding('fs')</code>.</span></p>
<p>[Zamiast nadpisywać wszystkie metody z wbudowanego modułu <code>fs</code>, ta biblioteka nadpisuje teraz <code>process.binding( 'fs' )</code>.]</p>
</blockquote>
<p>Prawdę mówiąc, nie słyszałem wcześniej o tajemniczym <code>process.binding()</code>, a <a href="https://nodejs.org/docs/latest-v20.x/api/process.html" rel="noreferrer noopener">oficjalna dokumentacja milczy na ten temat</a>. Na całe szczęście istnieje internet i <a href="https://stackoverflow.com/a/46908813/9025529" rel="noreferrer noopener">szybkie googlowanie dało mi odpowiedź</a>. Ta niepozorna funkcja stanowi łącznik między JS-owymi częściami Node’a, a tymi napisanymi w innych językach (głównie C++). W rzeczywistości bowiem wbudowany moduł <code>fs</code> to JS-owa nakładka na bibliotekę do operacji na plikach napisaną w C++. I w jakiś sposób ta nakładka musi się z tym kodem w innym języku porozumiewać. Tym jest właśnie <code>process.binding()</code> – zwraca funkcje pozwalające na wywołanie kodu C++! I z jakiegoś powodu jest ona dostępna z poziomu użytkownika Node’a. nie jedynie z wewnątrz samego Node’a.</p>
<p>Co więcej, zwracany binding można do woli nadpisywać, a zmiany są widoczne dla całego procesu, omijając przy tym problem oddzielnych wersji dla ESM i CJS. Tym samym nadpisać dowolną operację na plikach można także w następujący sposób:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { mkdir } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs/promises'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { binding } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:process'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fsBinding</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> binding</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">fsBinding.mkdir </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> console.error; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> mkdir</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'./whatever'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Importuję funkcję <code>binding()</code> z wbudowanego modułu <code>node:process</code>, reprezentującego aktualny proces (1). Następnie zapisuję sobie do zmiennej bindingi dla modułu <code>fs</code> (2). Potem nadpisuję metodę <code>mkdir()</code> z bindingów na wywołanie <code>console.error()</code> (3). W ramach testów importuję funkcję <code>mkdir()</code> z wbudowanego modułu <code>fs</code> (4), po czym wywołuję ją z argumentem <code>./whatever</code> (5). Warto przy tym zauważyć, że nadpisanie bindingu dla <code>mkdir</code> następuje <em>po</em> zaimportowaniu <code>mkdir()</code> z modułu <code>fs</code>.</p>
<p>Domyślnie <a href="https://nodejs.org/docs/latest-v20.x/api/fs.html#fspromisesmkdirpath-options" rel="noreferrer noopener">funkcja <code>mkdir</code> tworzy nowy katalog</a>. Jednak odpalenie powyższego kodu sprawi, że zamiast stworzyć nowy katalog, funkcja <code>mkdir()</code> po prostu wyświetli w konsoli argumenty przekazane jej bindingowi:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Shell</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">./whatever</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 511</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Symbol</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#6F42C1;--shiki-dark:#B392F0">fs_use_promises_symbol</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Co prawda rezultat, który otrzymujemy, nie do końca pokrywa się z tym, jakie parametry przyjmuje funkcja <code>mkdir()</code>, ale da się dość łatwo zgadnąć, który argument za co odpowiada:</p>
<ul>
<li><code>./whatever</code> to przekazana przez nas ścieżka do nowego katalogu,</li>
<li><code>511</code> to <a href="https://en.wikipedia.org/wiki/Chmod#Numerical_permissions" rel="noreferrer noopener">chmod</a> nowego katalogu,</li>
<li><code>false</code> to informacja, czy katalogi mają być tworzone rekursywnie (jeślibyśmy przekazali ścieżkę typu <code>./whatever/innyever</code>),</li>
<li><code>Symbol(fs_use_promises_symbol)</code> to oznaczenie, że wywołano obiecankową wersję funkcji <code>mkdir()</code>.</li>
</ul>
<p>W ten sposób udało nam się nadpisać funkcję <code>mkdir()</code> <em>w trakcie</em> działania programu. Gdybym wiedział o tym cudeńku wcześniej, to na pewno bym użył tego w trakcie pisania Bramkarza.</p>
<p>A tak… no cóż, kolejna cegiełka do mojej kolekcji ciekawej, acz średnio przydatnej wiedzy o JS-ie.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Piękny kod</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/piekny-kod" rel="alternate" type="text/html"/>
			<published>2023-09-26T22:08:00.000Z</published>
			<updated>2023-09-26T22:08:00.000Z</updated>
			<id>https://blog.comandeer.pl/piekny-kod</id>
			
				<summary><![CDATA[Jak można stworzyć własny formatter kodu JS?]]></summary>
			
			<content type="html"><![CDATA[<p>Ostatnio doszedłem do wniosku, że do formatowania mógłbym używać jakiegoś faktycznego formatera zamiast <a href="https://eslint.org/" rel="noreferrer noopener">ESLinta</a>. Oczywistym wyborem byłby <a href="https://prettier.io/" rel="noreferrer noopener">Prettier</a>, ale nie byłbym sobą, gdybym nie pisał swojego kodu w stylu, pod który nijak się nie da Prettiera skonfigurować. A nie uśmiechało mi się zmieniać styl pisania kodu tylko po to, żeby odhaczyć sobie na liście “rozpoczęcie używania formatera”. Zacząłem szukać alternatywy i… trafiłem tak naprawdę na jedną obiecującą – <a href="https://dprint.dev/" rel="noreferrer noopener">dprint</a>. Szybki, mocno konfigurowalny i ogólnie bardzo miły w pracy – tylko że brakowało mu <em>dosłownie</em> 3 opcji konfiguracyjnych, żeby formatował dokładnie tak, jak chcę.</p>
<p>Zatem zrobiłem tylko jedną logiczną rzecz… Co? Nie, oczywiście, że nie chodzi mi o przygotowanie PR-a do dprinta. Zacząłem rzeźbić <a href="https://github.com/Comandeer/formatter/" rel="noreferrer noopener">własny formater</a>!</p>
<h2 id="problem"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#problem">Problem</a></h2>
<p>Ale w zasadzie jaki problem chciałem rozwiązać formaterem? No w sumie to żaden… Po prostu zauważyłem, że zdecydowana większość rzeczy, na które zwraca mi uwagę ESLint, to właśnie formatowanie. Czyli akurat ta część pisania kodu, którą można najłatwiej zautomatyzować. Więc zacząłem się&nbsp;zastanawiać, czy nie byłoby wydajniej, gdybym nie musiał w ogóle martwić&nbsp;się podkreślaniem brakujących spacji wewnątrz tablic czy innymi przecinkami w nieodpowiednich miejscach. Zwłaszcza, że <a href="https://typescript-eslint.io/linting/troubleshooting/formatting/" rel="noreferrer noopener">nieużywanie lintera do formatera powoli staje się dobrą praktyką</a>.</p>
<p>No i formater pozwoliłby mi także na formatowanie bardziej złożonych przypadków, które przy pomocy ESLinta trudno obsłużyć. W końcu linter niekoniecznie musi nawet wiedzieć o tym, jak sformatowany jest kod – dla niego najważniejsza jest semantyka kodu. To formater jest choćby od tego, by wiedzieć, ile jest znaków w danej linijce i jak przełamać ciąg znaków, jeśli ktoś się uprze, że linia nie może mieć więcej niż 100 znaków.</p>
<p>Pierwszym krokiem do formaterowej rewolucji było <a href="https://github.com/Comandeer/eslint-config/blob/5a4b12f765f494e5358535063cefe0d12c836c68/src/formatting.js" rel="noreferrer noopener">wydzielenie reguł formatujących do osobnej konfiguracji ESLinta</a>. Dzięki temu będę w stanie je szybciej wyłączyć, gdy już podłączę formater. Drugim krokiem było przeglądnięcie istniejących opcji. A tych, wbrew pozorem, nie jest dużo. Jest oczywiście Prettier – król formaterów – ale sam nazywa siebie mocno <i lang="en">opinionated</i> (upartym), co jest eufemizmem na “powodzenia z konfigurowaniem”. Dlatego też odpadł w przedbiegach. Następny w kolejce był dprint. Trzeba przyznać – ma imponującą liczbę opcji konfiguracyjnych. Ale mimo to nie udało mi się znaleźć&nbsp;niektórych opcji pozwalających na np. wstawianie spacji wewnątrz definicji pętli. A że całość jest napisana w <a href="https://www.rust-lang.org/" rel="noreferrer noopener">Ruście</a>, w którym nie czuję się jakoś szczególnie mocny, to na ten moment dopisanie do dprinta potrzebnych mi ficzerów odłożyłem na półkę. Spojrzałem nawet na <a href="https://github.com/rome/tools" rel="noreferrer noopener">Rome</a>, niemniej pod względem konfiguracji było równie nieciekawie, co w przypadku Prettiera.</p>
<p>No więc została tylko jedna opcja: napisać to samemu!</p>
<h2 id="koncept"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#koncept">Koncept</a></h2>
<p>W gruncie rzeczy formater nie różni się zbytnio od pozostałych zabaw z JS-em, jakie ostatnio uskuteczniałem. Jego fundamentem jest – a jakże – <a href="https://blog.comandeer.pl/bujajac-sie-na-galezi-ast.html">AST</a>. Z tym, że dotąd interesowały mnie głównie transformacje samego drzewka, całkowicie nie zważałem na to, co ostatecznie się z tym drzewkiem dzieje. A zatem: nie interesowało mnie, jak będzie wyglądał wyprodukowany kod JS. W tym przypadku jest jednak odwrotnie: to właśnie ten generowany kod mnie najbardziej interesuje.</p>
<p>Na dobrą sprawę formater można nazwać generatorem kodu, który tym się&nbsp;różni od tradycyjnego generatora (takiego jak <a href="https://www.npmjs.com/package/@babel/generator" rel="noreferrer noopener"><code>@babel/generator</code></a>), że dba o to, aby zwracany kod był <em>czytelny</em> dla człowieka. Zasada działania generatora jest w miarę prosta:</p>
<ul>
<li>weź drzewko AST,</li>
<li>dla każdego węzła zwróć jego tekstową reprezentację,</li>
<li>posklejaj wszystkie te ciągi w jeden i go zwróć.</li>
</ul>
<h2 id="wyzwania"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#wyzwania">Wyzwania</a></h2>
<p>Brzmi dość prosto, ale okazuje się, że niekoniecznie takie jest.</p>
<h3 id="liczba-węzłów"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#liczba-węzłów">Liczba węzłów</a></h3>
<p>Pierwszym problemem, na jaki można się&nbsp;natknąć, jest przekształcanie węzłów do ich reprezentacji tekstowej. Weźmy prosty, przykładowy kod:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'test'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli spojrzymy na <a href="https://astexplorer.net/#/gist/85739028a46e9b5b442efe79e8b5483a/latest" rel="noreferrer noopener">wygenerowane drzewko</a>, zauważymy, że wcale nie jest takie proste. Całość opakowana jest w węzeł <code>File</code>, który reprezentuje plik JS. W tym pliku znajduje się z kolei <code>Program</code> – a więc całość kodu JS. Wewnątrz programu mamy <code>ExpressionStatement</code> (czyli instrukcję, która jest także wyrażeniem; to nasz <code>console.log()</code> wraz ze średnikiem na końcu), a dalej samo wyrażenie. Ale żeby nie było za łatwo, to wyrażenia mają&nbsp;swoje typy! W tym przypadku – <code>CallExpression</code>, czyli wywołanie funkcji. Ale samo wywołanie funkcji składa się z kolejnych dwóch węzłów – argumentu będącego <code>StringLiteral</code>em (ciągiem znaków) oraz nazwy wywoływanej funkcji. Ta nazwa z kolei to <code>MemberExpression</code>, czyli odwołanie do własności obiektu (bo tak naprawdę wywołujemy metodę <code>log()</code> obiektu <code>console</code>), a ten składa się&nbsp;z dwóch <code>Identifier</code>ów – nazwy obiektu oraz nazwy samej własności.</p>
<p>W postaci uproszczonego drzewka wyglądałoby to mniej więcej tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>File</span></span>
<span class="line"><span>| - Program</span></span>
<span class="line"><span>|   | - ExpressionStatement</span></span>
<span class="line"><span>|   |   | - CallExpression</span></span>
<span class="line"><span>|   |   |   | - StringLiteral</span></span>
<span class="line"><span>|   |   |   | - MemberExpression</span></span>
<span class="line"><span>|   |   |   |   | - Identifier</span></span>
<span class="line"><span>|   |   |   |   | - Identifier</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>W sumie: 7 rodzajów węzłów. A wywołaliśmy tylko <code>console.log()</code>a! Jasne, część z tego drzewka to niepotrzebne nam “śmieci” (jak <code>File</code> czy <code>Program</code>), ale reszty nie da się tak prosto pozbyć. Co oznacza, że nawet dla prostych programów trzeba dodać obsługę sporej liczby węzłów. Części z nich nie da się pominąć, ale niektóre rzeczy można próbować upraszczać (jak np. literały czy typowe przypisy z TS-a). Niemniej: na start widać, że to nie będzie przyjemna praca, tylko żmudne rzeźbienie w oceanie pojedynczych węzłów.</p>
<h3 id="wcięcia"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#wcięcia">Wcięcia</a></h3>
<p>Kolejnym, dość nieoczywistym problemem, są wcięcia. Prosty przykład:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> test</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Formater musi wiedzieć, że wewnątrz funkcji na początku nowej linii musi być wcięcie. I w takim prostym przypadku da się to zrobić “na chama”, wstawiając odpowiednie wcięcie na sztywno. Ale w bardziej skomplikowanych przypadkach już niekoniecznie to zadziała:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> test</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W każdym przypadku, gdy będzie więcej niż jeden poziom zagłębienia, formater musi wiedzieć, ile wcięć powinien wstawić. Zatem musi w jakiś sposób śledzić to, jak wygląda generowany kod.</p>
<h3 id="długość-linii"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#długość-linii">Długość linii</a></h3>
<p>Wreszcie – długość linii. Załóżmy, że chcemy, aby linie nie przekraczały 120 znaków. Tylko w jaki sposób formater miałby to wykryć? Zwłaszcza, że ta długość linii może zacząć przekraczać 120 znaków dopiero po <em>sformatowaniu</em>. Kolejny problem pojawia się przy pytaniu, co z tym zrobić. Formater musiałby wiedzieć, jak np. łamać ciągi znaków albo zapisywać całe funkcje w taki sposób, żeby np. rozdzielać parametry na linie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> test</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">	a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    b</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    c</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Ze wszystkich problemów ten wydaje się najbardziej złożony.</p>
<h2 id="implementacja"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#implementacja">Implementacja</a></h2>
<p>Zacząłem rzeźbić pomału <a href="https://github.com/Comandeer/formatter/" rel="noreferrer noopener">własny formater</a>. Z racji tego, że uwielbiam ironię, nie jest w żaden sposób konfigurowalny. Co ma w sumie sens, zważając na to, że <s>nigdy go nie ukończę</s> tylko ja go będę używał. Na ten moment rozwój idzie dość niemrawo. Okazało się bowiem, że stworzenie podstawowej architektury i rozwiązanie większości problemów zajmują bardzo mało czasu w porównaniu do… tworzenia obsługi poszczególnych typów węzłów. A to jest po prostu żmudna robota, która skutecznie mnie zniechęca do dłuższego grzebania przy tym.</p>
<p>Ale do rzeczy!</p>
<h3 id="formatery"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#formatery">Formatery</a></h3>
<p>Trzon rozwiązania stanowią tzw. <a href="https://github.com/Comandeer/formatter/tree/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/formatters" rel="noreferrer noopener">formatery</a> (“Słyszałem, że lubisz formatować swój kod, więc umieściłem formatery wewnątrz formatera, żebyś mógł formatować kod w trakcie jego formatowania!”) – czyli funkcje zajmujące się mieleniem poszczególnych węzłów do ich tekstowej reprezentacji. Każdy formater ma identyczną&nbsp;strukturę. Weźmy dla przykładu <a href="https://github.com/Comandeer/formatter/blob/main/src/formatters/BooleanLiteral.ts" rel="noreferrer noopener">ten dla <code>BooleanLiterala</code></a> (czyli wartości boolean):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { isBooleanLiteral } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '@babel/types'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { FormatterContext } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '../context.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> BooleanLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">context</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> FormatterContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> context;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#6F42C1;--shiki-dark:#B392F0">isBooleanLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		throw</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Incorrect node type'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> String</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.value ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Formater przyjmuje jako argument kontekst (o nim za chwilę) i zwraca ciąg znaków (1). Na samym początku następuje sprawdzenie, czy na pewno mamy do czynienia z booleanem (2) przy pomocy funkcji udostępnianej przez samego Babela (3). Jeśli nie, rzucamy błąd (4). Taka sytuacja nigdy nie powinna się&nbsp;zdarzyć, a samo sprawdzenie jest w sumie kompromisem, na jaki poszedłem z TS-em, żeby niepotrzebnie nie komplikować&nbsp;typów w innych miejscach. Dlatego praktycznie zawsze zwracana jest tekstowa reprezentacja węzła (5). W przypadku booleana całość to po prostu skonwertowanie wartości węzła do stringu. W innych przypadkach konwersja może być zdecydowanie bardziej skomplikowana.</p>
<div class="note" role="note" aria-labelledby="note-49"><p class="note__label" id="note-49">Dygresja</p><div class="note__content"><p>Tak, przesiadłem się na TS. Głównym powodem był… brak dobrych podpowiedzi w <a href="https://code.visualstudio.com/" rel="noreferrer noopener">VSC</a> dla czystego JS-a.</p></div></div>
<h3 id="konteksty"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#konteksty">Konteksty</a></h3>
<p>I właśnie w tych innych przypadkach przydają się konteksty. To <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/context.ts" rel="noreferrer noopener">prosta struktura</a>, która przechowuje aktualnie formatowany węzeł wraz z aktualnym stanem całego formatowania. Do tego dochodzą proste metody do wywołania formatowania na potomnym węźle (np. na argumencie, gdy jesteśmy wewnątrz deklaracji funkcji) czy do pobrania poprzedniego/następnego węzła w drzewie. Z kolei w stanie, na ten moment, znajdują&nbsp;się tylko dwie rzeczy: kod źródłowy JS-a oraz aktualny poziom zagnieżdżenia. Dzięki temu drugiemu udało się&nbsp;rozwiązać problem z wcięciami. Teraz zwiększenie poziomu wcięcia rozwiązuję, wywołując <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/context.ts#L18" rel="noreferrer noopener">metodę <code>context#formatDescendant()</code></a> z odpowiednim argumentem:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">context.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">formatDescendant</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	increaseIndent: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Opcja ta jest wykorzystywana np. do <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/formatters/BlockStatement.ts#L12-L16" rel="noreferrer noopener">automatycznego wcinania kodu wewnątrz bloków</a>.</p>
<p>Z kolei trzymanie kodu źródłowego w stanie pozwoliło na rozwiązanie dość nieoczekiwanego problemu. Nie tak dawno temu <a href="https://blog.comandeer.pl/makrony.html">opisywałem makra</a>. Korzystały one z nowej składni tzw. <a href="https://github.com/tc39/proposal-import-attributes" rel="noreferrer noopener">atrybutów importów</a>. Problem w tym, że wcześniej nazywało się to asercjami importów i używało innego słowa kluczowego:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Nowa składnia</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> test </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './test.js'</span><span style="color:#D73A49;--shiki-dark:#F97583"> with</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { type: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'whatever'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Stara składnia</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> test </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './test.js'</span><span style="color:#D73A49;--shiki-dark:#F97583"> assert</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { type: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'whatever'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Obecnie obydwie te wersje są wspierane przez silniki JS (zwłaszcza <a href="https://v8.dev/" rel="noreferrer noopener">V8</a>, którego używa m.in. Node.js), ale Babel reprezentuje je <em>w ten sam sposób</em>. Innymi słowy: z poziomu drzewka AST nie da się dowiedzieć, które słowo kluczowe zostało użyte. Na szczęście Babel przy parsowaniu pliku dołącza do każdego węzła informacje o tym, gdzie się znajduje jego kod źródłowy w tymże pliku. Dlatego też napisałem <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/utils/extractNodeFromCode.ts" rel="noreferrer noopener">prostą funkcję do wyciągania kodu danego węzła</a>. Następnie <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/formatters/ImportDeclaration.ts#L89-L90" rel="noreferrer noopener">przy pomocy wyrażenia regularnego przeszukuję ten kod</a> i ustalam, jakie słowo kluczowe zostało użyte. Brutalne, ale działa.</p>
<p>Z kolei funkcje pobierające poprzedni i następny węzeł pozwoliły mi na rozwiązanie innego problemu: ustalenia, gdzie wstawić puste linie w kodzie. Na ten moment jest to wykorzystywane w f<a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/formatters/IfStatement.ts#L12-L13" rel="noreferrer noopener">ormaterze <code>if</code>ów do generowania pustej linii przed nimi</a>. Ale zapewne użyję tego w wielu innych miejscach, bo dość chętnie stosuję puste linie.</p>
<h3 id="tło-fabularne"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#tło-fabularne">Tło fabularne</a></h3>
<p>Ale czemu w zasadzie konteksty powstały? Zapewne osoby trochę siedzące w JS-owym AST zauważą, że ich API jest dość podobne do <a href="https://github.com/jamiebuilds/babel-handbook/blob/c6828415127f27fedcc51299e98eaf47b3e26b5f/translations/en/plugin-handbook.md#toc-paths" rel="noreferrer noopener">ścieżek z Babela</a>. Niemniej ścieżki zawierają sporo informacji, które nie są dla mnie interesujące, a równocześnie – nie zawierają informacji, które są dla mnie istotne (np. poziom zagłębienia). Co więcej, oficjalne API Babela “odwiedza” węzły w kolejności ich występowania w kodzie, a bardzo szybko przekonałem się, że o wiele wygodniejsze byłoby dla mnie robienie tego rekurencyjnie (zatem nie “w bok”, a w głąb). Wróćmy do naszego <code>console.log()</code>a. Przy wykorzystaniu <a href="https://www.npmjs.com/package/@babel/traverse" rel="noreferrer noopener">oficjalnej paczki <code>@babel/traverse</code></a>, Babel będzie odwiedzał&nbsp;węzły w następującej kolejności:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>enter Program</span></span>
<span class="line"><span>enter ExpressionStatement</span></span>
<span class="line"><span>enter CallExpression</span></span>
<span class="line"><span>enter MemberExpression</span></span>
<span class="line"><span>enter Identifier</span></span>
<span class="line"><span>exit Identifier</span></span>
<span class="line"><span>enter Identifier</span></span>
<span class="line"><span>exit Identifier</span></span>
<span class="line"><span>exit MemberExpression</span></span>
<span class="line"><span>enter StringLiteral</span></span>
<span class="line"><span>exit StringLiteral</span></span>
<span class="line"><span>exit CallExpression</span></span>
<span class="line"><span>exit ExpressionStatement</span></span>
<span class="line"><span>exit Program</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Babel pozwala na określenie, czy chcemy obsłużyć węzeł przy wchodzeniu do niego (<code>enter</code>), czy przy wychodzeniu (<code>exit</code>). W przypadku <code>console.log();</code>a wejście oznacza dotarcie do <code>console</code>, a wyjście – dotarcie do średnika na końcu. Niemniej to oznacza, że generowanie np. <code>ExpressionStatement</code> musiałoby być podzielone na dwie części. Na wejściu, na dobrą sprawę, nie mielibyśmy pojęcia, jak wygląda instrukcja, moglibyśmy jedynie zaznaczyć w jakimś&nbsp;wewnętrznym stanie, że w takowej jesteśmy. Następnie każdy węzeł byłby uważany za część naszej instrukcji. I dopiero, gdy napotkalibyśmy wyjście z instrukcji, moglibyśmy ją&nbsp;w całości zwrócić. To może dość utrudniać poprawną&nbsp;obsługę wcięć i białych znaków, nie wspominając już o problemie z długością linii.</p>
<p>Taka metoda dodatkowo średnio pozwala na pominięcie węzłów, które z różnych powodów nas nie interesują. Dlatego doszedłem do wniosku, że o wiele lepiej byłoby móc przechodzić drzewo rekurencyjnie (w głąb). Stąd stworzyłem konteksty, które zastępują Babelowe ścieżki i pozwalają na ręczne ustalenie, czy chcemy wchodzić w głąb jakiegoś węzła. Każdy formater dostaje swój własny kontekst, zawierający węzeł do sformatowania wraz z aktualnym stanem formatowania i informacjami o najbliższym otoczeniu węzła. Kontekst pozwala także na wywołanie formatowania potomnego węzła – albo i nie, dzięki czemu nie trzeba obsługiwać wszystkich dziwnych przypadków lub <a href="https://github.com/Comandeer/formatter/blob/4d580bd6df1d2345319a56bac5ed17f61fdd3d1c/src/formatters/TSTypeAnnotation.ts#L11-L20" rel="noreferrer noopener">obsłużyć je w rodzicu</a>.</p>
<h3 id="nierozwiązany-problem-długości-linii"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#nierozwiązany-problem-długości-linii">Nierozwiązany problem długości linii</a></h3>
<p>Główny nierozwiązany problem, jaki pozostał, to problem zbyt długich linii. Zastanawiałem się, czy uda mi się znaleźć jakieś sensowne rozwiązanie dla niego, ale na tę chwilę nic mi nie przychodzi do głowy. Jedyne na co wpadłem, to sformatowanie danego węzła, po czym sprawdzenie, czy wynikowy ciąg ma więcej niż 120 znaków. W takim przypadku byłby uruchamiany “tryb wielolinowy”, który formatowałby dany węzeł tak, by ten mieścił się w określonej liczbie znaków. Jeśli nie wymyślę nic sensowniejszego, to prawdopodobnie ostatecznie spróbuję z tym sposobem.</p>
<h2 id="dalsze-plany"><a class="header-anchor" href="https://blog.comandeer.pl/piekny-kod#dalsze-plany">Dalsze plany</a></h2>
<p>Dalszych planów za bardzo nie ma. Prace uznam za zakończone, gdy uda mi się przepuścić&nbsp;przez formater kod <a href="https://github.com/Comandeer/rollup-lib-bundler" rel="noreferrer noopener">mojego bundlera</a> i dostać na wyjściu dokładnie taki sam kod, jaki był na wejściu. Ale do tego jeszcze długa droga, bo na ten moment formater obsługuje kilkanaście typów węzłów z jakichś <em>kilkuset</em> potrzebnych. Więc jest spora szansa, że zarzucę całość, zanim na dobre ją ukończę…</p>
<p>…albo dprint doda brakujące opcje konfiguracyjne. Jedno z tych dwóch.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Makrony</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/makrony" rel="alternate" type="text/html"/>
			<published>2023-06-26T19:35:00.000Z</published>
			<updated>2023-06-26T19:35:00.000Z</updated>
			<id>https://blog.comandeer.pl/makrony</id>
			
				<summary><![CDATA[Eksperymentalna implementacja makr z Buna w Rollupie.]]></summary>
			
			<content type="html"><![CDATA[<p>Nie tak dawno temu <a href="https://bun.sh/blog/bun-macros" rel="noreferrer noopener">Bun pokazał makra</a>. Spodobały mi się na tyle, że postanowiłem spróbować przenieść je do <a href="https://rollupjs.org/" rel="noreferrer noopener">Rollupa</a>.</p>
<h2 id="makra-co-to"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#makra-co-to">Makra –&nbsp;co to?</a></h2>
<p>Założenie makr jest dość proste: przy wykorzystaniu tzw. <a href="https://github.com/tc39/proposal-import-attributes" rel="noreferrer noopener">atrybutów importów</a> poinformujmy kompilator, że dana importowana funkcja jest makrem. Następnie kompilator znajdzie wszystkie wywołania tej funkcji i zamieni je na wynik jej wywołania.</p>
<p>Wyobraźmy sobie, że mamy moduł eksportujący funkcję losującą liczbę, <code>random.js</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> random</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">random</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { random };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Następnie w innym miejscu naszej aplikacji chcemy jej użyć jako makra:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { random } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './random.js'</span><span style="color:#D73A49;--shiki-dark:#F97583"> with</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { type: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'macro'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">random</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W imporcie pojawiła się dodatkowa część ze słówkiem kluczowym <code>with</code> – to właśnie atrybut importu. W tym przypadku nazywa się&nbsp;<code>type</code> i ma wartość <code>macro</code>, ponieważ chcemy poinformować kompilator, że ma do czynienia z makrem. W podobny sposób można poinformować, że wczytujemy np. JSON zamiast JS-a:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pkg </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './package.json'</span><span style="color:#D73A49;--shiki-dark:#F97583"> with</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { type: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'json'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Warto przy tym zauważyć, że na ten moment zdecydowanie częściej można spotkać&nbsp;starszą wersję składni atrybutów (z czasów, gdy jeszcze były asercjami):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pkg </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './package.json'</span><span style="color:#D73A49;--shiki-dark:#F97583"> assert</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { type: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'json'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Obydwie robią to samo, ale ta druga ma obecnie lepsze wsparcie zarówno w środowiskach uruchomieniowych JS-a, jak i we wszelkiego rodzaju narzędziach. To się jednak będzie powoli zmieniać, zatem pozwolę&nbsp;sobie używać&nbsp;w tym artykule nowszej składni.</p>
<div class="note" role="note" aria-labelledby="note-46"><p class="note__label" id="note-46">Dygresja</p><div class="note__content"><p>Zapewne niektóre osoby może zdziwić, że nowa składnia używa słowa kluczowego <code>with</code>, które <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with" rel="noreferrer noopener">istnieje w JS-ie od zawsze</a>. Niemniej jest ono zabronione w trybie ścisłym, a wszystkie moduły są w takim uruchamiane. Innymi słowy: <code>with</code> w imporcie jest całkowicie bezpieczne składniowo, bo prastary mechanizm JS-owy i tak nie może wystąpić w jego pobliżu.</p></div></div>
<p>Funkcje zaimportowane z atrybutem <code>{ type: 'macro' }</code> zostaną usunięte z ostatecznego kodu i zastąpione wynikiem ich wywołania:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7507075485199182</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W bunie dzieje się to podczas wywołania komendy <code>build</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Shell</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">bun</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> build</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ./app.ts</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W Node.js nie ma jednak wbudowanego żadnego buildera czy choćby kompilatora TS-a, ale za to istnieje bogaty ekosystem bundlerów. Jednym z nich jest Rollup.js. I właśnie do niego postanowiłem dodać eksperymentalną obsługę makr.</p>
<h2 id="zarys-rozwiązania"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#zarys-rozwiązania">Zarys rozwiązania</a></h2>
<p>Rollup.js jest narzędziem, które bardzo łatwo rozwijać dzięki jego dość rozbudowanemu <a href="https://rollupjs.org/plugin-development/" rel="noreferrer noopener">API pluginów</a>. W taki też sposób można do niego dodać obsługę makr. Tak naprawdę plugin będzie składał się&nbsp;z dwóch części:</p>
<ol>
<li>wykrycia, że dany moduł to moduł zawierający makra, dzięki czemu będzie można go usunąć&nbsp;z ostatecznego bundle’a,</li>
<li>podmiany wywołań konkretnych makr na ich wyniki.</li>
</ol>
<p>Obydwie części wykonamy przy pomocy tzw. <a href="https://rollupjs.org/plugin-development/#build-hooks" rel="noreferrer noopener">hooków (haków)</a>, które pozwalają się wpinać w poszczególne etapy bundle’owania, takie jak parsowanie opcji przekazanych do rollupa czy wczytywanie poszczególnych modułów. Do różnych operacji na kodzie wykorzystamy <a href="https://blog.comandeer.pl/bujajac-sie-na-galezi-ast.html">AST</a>.</p>
<p>Bierzmy się&nbsp;zatem do pracy!</p>
<h2 id="wykrywanie-modułu-z-makrami"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#wykrywanie-modułu-z-makrami">Wykrywanie modułu z makrami</a></h2>
<p>W przypadku Rollupa nie da się tak po prawdzie całkowicie usunąć jakiegoś modułu z bundle’a. Jedyne, co można zrobić, to poinformować bundler, że dany moduł jest <i lang="en">external</i> (zewnętrzny). To sprawi, że Rollup pozostawi import tego modułu w spokoju. Jest to zdecydowanie krok w dobrą stronę, bo o wiele prościej będzie usunąć taki import, niż próbować usunąć&nbsp;cały zbundle’owany kod modułu.</p>
<p>Wykrywanie modułu z makrami jest stosunkowo proste. Rollup udostępnia <a href="https://rollupjs.org/plugin-development/#resolveid" rel="noreferrer noopener">hook <code>resolveId()</code></a>, który pozwala m.in. właśnie na oznaczenie modułu jako zewnętrzny.</p>
<p>Ale zanim przejdziemy do mięska, trzeba stworzyć szkielet naszego pluginu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'macros'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> plugin;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Typowy plugin do Rollupa jest funkcją (1), która zwraca obiekt (2). Musi on mieć własność name (3) zawierającą nazwę pluginu. Pozostałymi własnościami są poszczególne hooki.</p>
<p>Dodajmy zatem nasz hook <code>resolveId()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'macros'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		resolveId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">importee</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">importer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#E36209;--shiki-dark:#FFAB70">assertions</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( assertions </span><span style="color:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> assertions.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'macro'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					id: importee, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					external: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> plugin;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Hook <code>resolveId()</code> przyjmuje kilka parametrów (1):</p>
<ol>
<li><code>importee</code> – nazwa wczytywanego modułu,</li>
<li><code>importer</code> – ścieżka wczytującego modułu,</li>
<li><code>options</code> – obiekt z dodatkowymi informacjami o wczytywanym module, w tym atrybuty (nazywane tutaj asercjami – <code>assertions</code>).</li>
</ol>
<p>Nas interesują w zasadzie tylko dwie rzeczy – nazwa wczytywanego modułu oraz atrybuty. Jeśli istnieje atrybut <code>type</code> i ma on wartość <code>macro</code> (2), zwracamy obiekt (3), który wskazuje, że ten konkretny moduł <code>importee</code> (4) powinien być traktowany jako zewnętrzny (5) i jego kod nie powinien zostać dorzucony do bundle’a. Z kolei jeśli nie ma atrybutu <code>type</code> albo ma on inną wartość, wówczas zwracamy <code>null</code> (6), co dla Rollupa oznacza, że dla tego modułu powinien odpalić hooki <code>resolveId()</code> z kolejnych pluginów albo użyć domyślnego, jeśli żadnego innego pluginu nie ma.</p>
<p>Taki hook <code>resolveId()</code> wystarczy, żeby import modułu z makrem został nienaruszony, dzięki czemu można go będzie łatwo usunąć.</p>
<h2 id="usuwanie-importów-modułów-z-makrami"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#usuwanie-importów-modułów-z-makrami">Usuwanie importów modułów z makrami</a></h2>
<p>Modyfikacje kodu wczytywanych modułów przeprowadza się w <a href="https://rollupjs.org/plugin-development/#transform" rel="noreferrer noopener">hooku <code>transform()</code></a>. Daje on dostęp do kodu modułu i oczekuje, że po wszystkich zmianach zwrócony zostanie nowy kod. Rollup pozwala także zwrócić mapę dla kodu po zmianach, co może być przydatne, jeśli zaszły większe zmiany. W przypadku tego eksperymentu pozwoliłem sobie pominąć mapy, bo niepotrzebnie komplikowałyby całość.</p>
<p>Dodajmy zatem hook <code>transform()</code> do naszego pluginu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'macros'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> plugin;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na ten moment potrzebny będzie nam tylko pierwszy argument, czyli kod modułu w formie stringa (1).</p>
<p>Skoro mamy stringa z kodem, to w teorii można by wykorzystać wyrażenia regularne, żeby znaleźć interesujący nas fragment i go podmienić (jest nawet <a href="https://www.npmjs.com/package/magic-string" rel="noreferrer noopener">biblioteka od tego</a> – swoją&nbsp;drogą, bardzo dobra!). Niemniej takie wyrażenie regularne musiałoby być mocno skomplikowane (bo samych sposobów wstawienia spacji między poszczególnymi elementami importu jest co najmniej kilka…), przez co całość byłaby podatna na błędy. Na szczęście istnieje inny, bardziej elegancki sposób: sparsowanie kodu do postaci AST.</p>
<p>Tutaj z pomocą przychodzi nam Rollup, który wewnątrz hooka <code>transform()</code> udostępnia <a href="https://rollupjs.org/plugin-development/#this-parse" rel="noreferrer noopener">metodę <code>this.parse()</code></a>. Wykorzystuje ona <a href="https://www.npmjs.com/package/acorn" rel="noreferrer noopener">parser Acorn</a> (używany wewnętrznie przez Rollupa), aby sparsować kod do drzewka AST. Niemniej samo drzewko nie da nam zbyt dużo, bo brakuje nam jeszcze sposobu, aby poruszać się po nim. Tego Rollup już nie oferuje, ale na szczęście istnieją&nbsp;odpowiednie narzędzia do tego. Na potrzeby tego eksperymentu użyjemy <a href="https://www.npmjs.com/package/estree-walker" rel="noreferrer noopener">pakietu <code>estree-walker</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { walk } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'estree-walker'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ast</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			walk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'ImportDeclaration'</span><span style="color:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isMacroImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">						this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">remove</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zamieniamy kod na AST przy pomocy wspomnianego już&nbsp;<code>this.parse()</code> (1). Następnie przechodzimy przez to drzewko przy pomocy funkcji <code>walk</code> (2), którą&nbsp;zaimportowaliśmy z pakietu <code>estree-walker</code> (3). Szukamy importów do makr (4). Jeśli takowy znajdziemy, usuwamy go (5). Szukanie składa się z dwóch zasadniczych części: sprawdzenia, czy typ węzła to <code>ImportDeclaration</code>, oraz wywołania funkcji <code>isMacroImport()</code>.</p>
<p>Logika tej funkcji jest bardzo podobna do tej, którą zastosowaliśmy w hooku <code>resolveId()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isMacroImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">node.assertions ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ </span><span style="color:#005CC5;--shiki-dark:#79B8FF">assertion</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.assertions; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> assertion.key.name </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'type'</span><span style="color:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> assertion.value.value </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'macro'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Sprawdzamy, czy import ma atrybuty (1) – czyli własność&nbsp;<code>node.assertions</code> (uwielbiam chaos w nazewnictwie!). Jeśli tak, pobieramy pierwszy z nich (2) i sprawdzamy, czy ma odpowiednią nazwę i wartość (3).</p>
<div class="note" role="note" aria-labelledby="note-47"><p class="note__label" id="note-47">Dygresja</p><div class="note__content"><p>Do zobaczenia, jak wygląda AST od środka, polecam narzędzie <a href="https://astexplorer.net/" rel="noreferrer noopener">AST Explorer</a>. Co prawda akurat atrybuty importów to ta jedna rzecz, której jeszcze nie obsługuje, ale przy całej reszcie zabaw z AST na potrzeby tego eksperymentu jest jak znalazł.</p></div></div>
<h2 id="zapisywanie-informacji-o-makrach"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#zapisywanie-informacji-o-makrach">Zapisywanie informacji o makrach</a></h2>
<p>Niemniej  jeśli ot tak usuniemy wszystkie importy makr, pozbawimy się informacji o tym, które wywołania funkcji powinniśmy następnie podmienić. Wypada zatem zapisać sobie gdzieś informacje z importów. W tym celu stwórzmy mapę makr:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { resolve </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> resolvePath, dirname } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ast</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macros</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			walk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'ImportDeclaration'</span><span style="color:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isMacroImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node ) ) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						extractMacros</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( macros, node, path );</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">						this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">remove</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> extractMacros</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">macros</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">modulePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> moduleDirPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> dirname</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( modulePath ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macroPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( moduleDirPath, node.source.value ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	node.specifiers.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">specifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> originalName</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> specifier.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'ImportDefaultSpecifier'</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			'default'</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			specifier.imported.name; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		macros.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">set</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( specifier.local.name, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			name: originalName,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			path: macroPath</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Makra będziemy zapisywali do zmiennej <code>macros</code> (1), w której znajduje się pusta mapa. Logika do zapisu znajduje się w funkcji <code>extractMacros()</code> (2). Przekazujemy do niej mapę, węzeł oraz ścieżkę do aktualnie transformowanego modułu. Ścieżkę tę dostajemy jako drugi  argument hooku <code>transform()</code> (3). Funkcja <code>extractMacros()</code> dla każdego specyfikatora importu (4) ustala jego oryginalną nazwę (5), a następnie zapisuje tę informację wraz ze ścieżką do importowanego modułu pod lokalną nazwą makra (6). Ścieżkę uzyskujemy poprzez pobranie katalogu obecnie transformowanego modułu (7) a następnie rozwiązując ścieżkę do importowanego modułu (wyciągniętą z własności <code>node.source.value</code>) względem niego (8) przy pomocy odpowiednich funkcji z modułu <code>node:path</code> (9).</p>
<p>Brzmi to skomplikowanie, więc rozbijmy to na części. Zacznijmy od specyfikatorów (<i lang="en">specifiers</i>). W składni importu to określenie na to, co importujemy, np:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { test } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './test.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Specyfikatorem w tym kodzie jest <code>test</code>. W tym konkretnym przypadku lokalna nazwa (czyli ta używana w module importującym), jak i ta oryginalna (czyli ta użyta do eksportu w module importowanym), są takie same. Ale nazwy te mogą się też różnić, gdy pojawią się aliasy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { test </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> t } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './test.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym wypadku lokalna nazwa to <code>t</code>, a oryginalna – <code>test</code>. Z racji tego, że nazwy te mogą się różnić między sobą, musimy zapisać&nbsp;je obie.</p>
<p>Warto też&nbsp;zwrócić&nbsp;uwagę na szczególny przypadek specyfikatora – specyfikator domyślnego importu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Whatever </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './whatever.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W takim wypadku węzeł nie zawiera oryginalnej nazwy, a jedynie lokalną. Nasz kod ma specjalny warunek na tę okazję:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> originalName</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> specifier.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'ImportDefaultSpecifier'</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 1</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'default'</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	specifier.imported.name; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli typ specyfikatora to <code>ImportDefaultSpecifier</code> (1) – czyli domyślny import – wówczas użyj <code>default</code> (2) jako oryginalnej nazwy. W przeciwnym razie weź nazwę z własności <code>imported</code> specyfikatora (3).</p>
<p>Ta sztuczka pozwoli nam później, przy samym wywoływaniu makra, nieco uprościć kod. Domyślne importy bowiem to w rzeczywistości skrócona wersja takiego zapisu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#D73A49;--shiki-dark:#F97583">default</span><span style="color:#D73A49;--shiki-dark:#F97583"> as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Whatever } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './whatever.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Inny słowy: domyślne importy można traktować jak aliasy do importu nazwanego eksportu <code>default</code>.</p>
<p>Z kolei w przypadku ścieżki potrzebujemy absolutnej (czemu, to okaże się później) – a więc rozpoczynającej się od <code>/</code>. Niemniej nasz węzeł importu zawiera ją najprawdopodobniej w formie ścieżki relatywnej – zaczynającej się od <code>./</code>. Takie ścieżki zawsze podawane są względem katalogu modułu importującego. Zatem przy pomocy <a href="https://nodejs.org/api/path.html#pathdirnamepath" rel="noreferrer noopener">funkcji <code>dirname()</code></a> wyciągamy absolutną ścieżkę do katalogu ze ścieżki do modułu, a następnie rozwiązujemy ścieżkę do modułu z makrami przy pomocy <a href="https://nodejs.org/api/path.html#pathresolvepaths" rel="noreferrer noopener">funkcji <code>resolvePath()</code></a>. Dzięki temu uzyskujemy absolutną&nbsp;ścieżkę do importowanego modułu.</p>
<div class="note" role="note" aria-labelledby="note-48"><p class="note__label" id="note-48">Dygresja</p><div class="note__content"><p>Moje przygody z ESM sugerują, że żeby kod ze ścieżkami działał poprawnie na wielkiej trójcy systemów (Linux, macOS, Windows), wypada zawsze wymuszać ścieżki POSIX-owe – czyli z <code>/</code> zamiast <code>\</code>. Inaczej w którymś momencie niemal na pewno coś się&nbsp;wywali z powodu złego slasha. Od siebie polecam <a href="https://www.npmjs.com/package/pathe" rel="noreferrer noopener">pakiet <code>pathe</code></a>, który ma identyczne API jak <code>node:path</code> i można go używać jako bezpośredni zamiennik.</p></div></div>
<p>Tak zebrane informacje wrzucamy do modułu z makrami i jesteśmy gotowi, by odpalić jakieś makro!</p>
<h2 id="wyszukiwanie-makr-w-kodzie"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#wyszukiwanie-makr-w-kodzie">Wyszukiwanie makr w kodzie</a></h2>
<p>Zanim jednak je odpalimy, musimy je znaleźć. Na szczęście istnieje  odpowiedni typ węzła, którego możemy po prostu poszukać w drzewku AST – <code>CallExpression</code>. Te węzły to nic innego jak wywołania funkcji.</p>
<p>Dodajmy zatem kolejny <code>if</code> do naszego <code>walka()</code>, tym razem dla wywołań funkcji:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ast</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macros</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			walk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">					// […]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'CallExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> name</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.callee.name; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">macros.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">has</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( name ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macro</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> macros.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( name ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Po upewnieniu się, że mamy do czynienia z wywołaniem funkcji (1), pobieramy jej nazwę (2). Sprawdzamy, czy taka nazwa występuje w naszej mapie z makrami (3). Jeśli tak, wyciągamy informacje o makrze do zmiennej macro (4).</p>
<h2 id="odpalanie-makr"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#odpalanie-makr">Odpalanie makr</a></h2>
<p>Skoro już znaleźliśmy makro, pora je odpalić:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">			// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			asyncWalk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				async</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">					// […]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'CallExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> name</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.callee.name;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">macros.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">has</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( name ) ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macro</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> macros.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( name );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macroResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> executeMacro</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( macro ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Cała logika odpalania makra została zamknięta w asynchronicznej funkcji <code>executeMacro()</code> (1), do której przekazujemy obiekt makra, a która zwraca <code>Promise</code> z wynikiem wywołania makra. Warto przy tym zwrócić uwagę, że to wymusza zrobienie całej metody <code>enter()</code> asynchroniczną (2). Równocześnie wymusza też wymianę&nbsp;<code>walk()</code> na <code>asyncWalk()</code> (3) – bo to pierwsze jest wyłącznie synchroniczne.</p>
<p>Natomiast funkcja <code>executeMacro()</code> prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { writeFile } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs/promises'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { pathToFileURL } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:url'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Worker } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:worker_threads'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { temporaryFile } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'tempy'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> executeMacro</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> alias</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'default'</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'tempName'</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pathURL</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> pathToFileURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( path );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> code</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `import { parentPort } from 'node:worker_threads';</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	import { ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } as ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } } from '${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pathURL</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }';</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	const result = await ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	parentPort.postMessage( result );`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> workerFilePath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> temporaryFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		extension: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'mjs'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> writeFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( workerFilePath, code, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">reject</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> worker</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Worker</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( workerFilePath );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'message'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, resolve );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'error'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, reject );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'exit'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">exitCode</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( exitCode </span><span style="color:#D73A49;--shiki-dark:#F97583">!==</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				reject</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( exitCode );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wykorzystuje ona <a href="https://nodejs.org/api/worker_threads.html" rel="noreferrer noopener">workery</a>, aby odpalić kod makra “z dala” od reszty kodu i następnie jedynie przekazać jego wynik.</p>
<p>Trochę się&nbsp;tutaj dzieje, więc rozbijmy sobie tę funkcję na dwie części. Pierwsza część odpowiedzialna jest za wygenerowanie kodu workera:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> alias</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'default'</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'tempName'</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pathURL</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> pathToFileURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( path ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> code</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `import { parentPort } from 'node:worker_threads';</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">import { ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } as ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } } from '${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pathURL</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }';</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">const result = await ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">parentPort.postMessage( result );`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> workerFilePath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> temporaryFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	extension: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'mjs'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> writeFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( workerFilePath, code, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku ustalamy lokalną nazwę importu – alias (1). Jeśli mamy do czynienia z domyślnym importem (czyli <code>default</code>), naszym aliasem będzie <code>tempName</code>. Wynika to z tego, że <code>default</code> jest <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words" rel="noreferrer noopener">zarezerwowanym słowem</a> w JS-ie i nie może ot tak wystąpić jako nazwa funkcji. Dlatego też musimy wybrać jakąś inną nazwę. Nie chciało mi się myśleć, więc jest <code>tempName</code>. Z kolei jeśli nazwa importu jest inna, używamy jej bezpośrednio. Następnie zamieniamy absolutną&nbsp;ścieżkę modułu na URL (2) przy pomocy <a href="https://nodejs.org/api/url.html#urlpathtofileurlpath" rel="noreferrer noopener">funkcji <code>pathToFileURL()</code></a> z modułu <code>node:url</code>. Moduły w Node.js wymagają, by były wczytywane po URL-u. A to oznacza, że ścieżki na Windowsie (nawet zapisane z <code>/</code> zamiast <code>\</code>) będą sprawiać problemy – a to z powodu tego, że litera dysku (np. <code>c:</code>) zostanie potraktowana jako <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL#scheme" rel="noreferrer noopener">schemat</a>. Dlatego bezpieczniej jest przekształcić absolutną&nbsp;ścieżkę na URL ze schematem <code>file:</code>. Mając przygotowany alias i URL modułu generujemy kod workera (3). Następnie tworzymy tymczasowy plik (4) z rozszerzeniem <code>.mjs</code> (5) – tutaj przy pomocy funkcji <code>temporaryFile()</code> z <a href="https://www.npmjs.com/package/tempy" rel="noreferrer noopener">pakietu <code>tempy</code></a>. Do tego pliku zapisujemy wygenerowany kod (6).</p>
<p>Wykorzystanie tymczasowego pliku tłumaczy, czemu potrzebowaliśmy absolutnej ścieżki do modułu z makrami. Taki tymczasowy plik jest tworzony w <a href="https://nodejs.org/api/os.html#ostmpdir" rel="noreferrer noopener">systemowym katalogu przeznaczonym do -przechowywania plików tymczasowych</a>. Z tego też powodu relatywna ścieżka wskazywałaby na nieistniejący plik. Ścieżka absolutna zapewnia z kolei poprawne wczytanie modułu.</p>
<p>Prawdę mówiąc, miałem nadzieję, że uda się całość zrobić <em>bez</em> użycia tymczasowego pliku, zwłaszcza, że konstruktor workera pozwala podać kod:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Worker</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'console.log( "Hello, world!" );'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	eval: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Problem polega na tym, że tak wykonany kod odpala się jako moduł CJS – a więc nie działają w nim importy.</p>
<p>W teorii można też przekazać do konstruktora <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs" rel="noreferrer noopener">Data URL</a> z kodem JS, ale ta opcja też nie chciała działać. Node.js upierał się, że URL jest niepoprawny, podczas gdy przeglądarka nie miała problemu z jego odpaleniem. Ostatecznie zatem musiałem użyć tymczasowego pliku. Na wszelki wypadek dałem mu rozszerzenie <code>.mjs</code>, żeby wymusić na Node.js traktowanie go jako modułu ES.</p>
<p>Przyjrzyjmy się jeszcze przez chwilę samemu kodowi workera:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { parentPort } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:worker_threads'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { name </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'file:///path/to/module-with-macros.mjs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> alias</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">parentPort.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">postMessage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( result ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jest bardzo prosty, bo jego jedynym zadaniem jest zaimportowanie makra (1), wywołanie go – na wszelki wypadek z <code>await</code>em (2), a następnie przesłanie do bundlera wyniku tego wywołania (3). W tym celu posługuje się <a href="https://nodejs.org/api/worker_threads.html#workerparentport" rel="noreferrer noopener">obiektem <code>parentPort</code></a> zaimportowanym z modułu <code>node:worker_threads</code> (4). Ten schemat jest bardzo podobny do tego, jak działa <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#sending_messages_to_and_from_a_dedicated_worker" rel="noreferrer noopener">komunikacja między głównym wątkiem a workerem w przeglądarce</a>.</p>
<p>Z kolei druga część&nbsp;funkcji <code>executeMacro()</code> wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">reject</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> worker</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Worker</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( workerFilePath ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'message'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, resolve ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'error'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, reject ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	worker.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'exit'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">exitCode</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( exitCode </span><span style="color:#D73A49;--shiki-dark:#F97583">!==</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			reject</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( exitCode ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zwraca ona <code>Promise</code> (1), dzięki czemu można poczekać na wykonanie <code>executeMacro()</code> przy pomocy zwykłego <code>await</code>. Wewnątrz tego <code>Promise</code>’a tworzymy workera na podstawie naszego tymczasowego pliku (2). Jeśli prześle on jakąś wiadomość, rozwiązujemy obietnicę z nią jako wynikiem (3). W przypadku błędu (4) lub przedwczesnego zakończenia workera (5) odrzucamy obietnicę.</p>
<h2 id="podmiana-makra-na-wynik"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#podmiana-makra-na-wynik">Podmiana makra na wynik</a></h2>
<p>Skoro już wywołaliśmy makro i dostaliśmy jego wynik, pora zamieścić go w kodzie zamiast samego wywołania. Tutaj jednak pojawia się problem: z workera otrzymujemy JS-ową wartość – czy to&nbsp;typu prostego, czy też obiekt lub tablicę. A my potrzebujemy węzła, który można wsadzić do drzewka AST. Tu z pomocą przychodzi nam nasz stary znajomy –&nbsp;<code>this.parse()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">			// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			asyncWalk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				async</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> parse</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.parse; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">					// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'CallExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">						// […]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macroResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> executeMacro</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( macro );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macroResultAST</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`( ${</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> JSON</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="color:#032F62;--shiki-dark:#9ECBFF">( </span><span style="color:#24292E;--shiki-dark:#E1E4E8">macroResult</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ) </span><span style="color:#032F62;--shiki-dark:#9ECBFF">} )`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> expression</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getValueNode</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( macroResultAST ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">						this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( expression ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wywołujemy <code>parse()</code> na wyniku zwróconym przez makro (1). Z racji tego, że parser przyjmuje wyłącznie stringi, najpierw musimy przepuścić&nbsp;ten wynik przez <code>JSON.stringify()</code>. Dodatkowo otaczamy całość nawiasami. Jest to konieczne, ponieważ w JS-ie istnieje pewna dwuznaczność – <code>{ a: 1 }</code> może oznaczać zarówno obiekt z własnością <code>a</code>, jak i tzw. <a href="https://blog.comandeer.pl/goto.html#etykiety">instrukcję z etykietą</a> zamkniętą w bloku. W przypadku tej niejasności parser wybiera instrukcję z etykietą w bloku… Otoczenie całości nawiasami usuwa tę dwuznaczność, ponieważ instrukcje nie mogą znajdować się wewnątrz nawiasów, a więc musi to być tzw. wyrażenie obiektowe (czyli inaczej – po prostu obiekt). Warto tutaj też zauważyć, że zapisaliśmy sobie <code>this.parse()</code> do zmiennej <code>parse()</code> (2) – a to dlatego, że wewnątrz <code>enter()</code> mamy inny kontekst <code>this</code> (węzeł AST). Samo sparsowanie wyniku do AST to jednak nie wszystko. Parser zwraca bowiem cały program, a takowym nie możemy zastąpić wywołania funkcji. Musimy wyłuskać&nbsp;z programu sam wynik – i tym zajmuje się funkcja <code>getValueNode()</code> (3). Gdy już&nbsp;wyłuskamy odpowiedni węzeł, zastępujemy nim ten obecny (4).</p>
<p>Funkcja <code>getValueNode()</code> wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getValueNode</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> allowedNodeTypes</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		'Literal'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		'ObjectExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		'ArrayExpression'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> valueNode; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	walk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">allowedNodeTypes.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.type ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			valueNode </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 10</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">skip</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 11</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> valueNode; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 12</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Przekazujemy do niej nasz wynik w formie AST (1). Następnie definiujemy sobie, jakich węzłów szukamy (2) – w naszym przypadku będą to literały (3), obiekty (4) oraz tablice (5). Dla parsera literałem są praktycznie wszystkie wartości prymitywne (oprócz symboli; niemniej tych i tak nie da się przesłać z workera, więc <span lang="en">who cares</span>). Następnie tworzymy sobie zmienną <code>valueNode</code> (6) i przechodzimy przez nasze drzewko przy pomocy znanej już nam funkcji <code>walk()</code> (7). Jeśli węzeł nie jest jednym z dozwolonych typów (8), nic nie robimy (9). W innym wypadku przypisujemy go do zmiennej <code>valueNode</code> (10) oraz wywołujemy <code>this.skip()</code> (11), informując walkera, że nie chcemy wchodzić głębiej w drzewko. Na samym końcu zwracamy znaleziony węzeł (12).</p>
<h2 id="obsługa-argumentów"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#obsługa-argumentów">Obsługa argumentów</a></h2>
<p>Niemniej niektóre makra mają parametry. I wypadałoby takie też obsługiwać. Na szczęście dodanie podstawowego wsparcia dla przekazywania argumentów do makr jest stosunkowo proste. I można to zrobić np. tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">			// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			asyncWalk</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast, {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				async</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">					// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">					if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.type </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'CallExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">						// […]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> allowedArgNodeTypes</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">							'Literal'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">							'ObjectExpression'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">							'ArrayExpression'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> args</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.arguments; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> isSupported</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> args.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">every</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">type</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> allowedArgNodeTypes.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( type ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">isSupported ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							throw</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Only macros with arguments of primitive types, object and arrays are supported currently.'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">						const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> macroResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> executeMacro</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( macro, args ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">						// […]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy sobie tablicę typów argumentów, które obecnie obsługujemy (1) – to dokładnie te same typy, które obsługujemy przy wyniku. Następnie do zmiennej <code>args</code> pobieramy sobie argumenty przekazane do makra z własności <code>node.arguments</code> (2). Przy pomocy <code>args.every()</code> (3) sprawdzamy, czy wszystkie argumenty są przez nas obsługiwane (4). Jeśli nie (5), rzucamy błąd (6). W innym przypadku wywołujemy makro, przekazując argumenty jako drugi argument (7).</p>
<p>W <code>executeMacro()</code> pojawił się z kolei kod odpowiedzialny za formatowanie argumentów:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { generate } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'escodegen'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> executeMacro</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }, </span><span style="color:#E36209;--shiki-dark:#FFAB70">args</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	// […]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> formattedArgs</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> args.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> generate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">', '</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> code</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `import { parentPort } from 'node:worker_threads';</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	import { ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } as ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } } from '${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pathURL</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }';</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	const result = await ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> alias</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }( ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> formattedArgs</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } ); // 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	parentPort.postMessage( result );`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	// […]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dla każdego argumentu w formie AST (1) generujemy odpowiadający mu kod przy pomocy funkcji <code>generate()</code> (2) z pakietu <code>escodegen</code> (3). Następnie łączymy wszystkie argumenty przy pomocy przecinka (4) – żeby uzyskać poprawny fragment kodu z argumentami. Mając takowy, podstawiamy go do wywołania makra w kodzie workera (5).</p>
<p><a href="https://www.npmjs.com/package/escodegen" rel="noreferrer noopener">Pakiet <code>escodegen</code></a> to taka odwrotność&nbsp;parsera – zwraca kod z podanego drzewka AST. Wykorzystaliśmy go tutaj, ponieważ do workera potrzebujemy kodu JS, nie – AST.</p>
<h2 id="zwrócenie-zmodyfikowanego-kodu"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#zwrócenie-zmodyfikowanego-kodu">Zwrócenie zmodyfikowanego kodu</a></h2>
<p>Skoro już wywołaliśmy makro i podstawiliśmy za nie jego wynik, została ostatnia rzecz: zwrócenie zmodyfikowanego kodu z hooka <code>transform()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> plugin</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">			// […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				code: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">generate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ast ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Raz jeszcze wykorzystujemy tutaj funkcję&nbsp;<code>generate()</code> (1) z pakietu <code>escodegen</code>, żeby wygenerować kod ze zmodyfikowanego przez nas drzewka AST.</p>
<p>I tym sposobem zaimplementowaliśmy makra w Rollupie!</p>
<h2 id="wnioski"><a class="header-anchor" href="https://blog.comandeer.pl/makrony#wnioski">Wnioski</a></h2>
<p>To był zadziwiająco przyjemny i stosunkowo mało złożony projekt. A efekt końcowy jest zdecydowanie jednym z lepszych, jakie udało mi się osiągnąć w przypadku moich eksperymentów. Jasne, jest tu sporo niedoskonałości, które można doszlifować, np.</p>
<ul>
<li>Co jeśli makro zostało przykryte przez jakąś&nbsp;zmienną w zasięgu? Wówczas nie powinniśmy podmieniać takiego wywołania. Tutaj na pomoc może przyjść <a href="https://www.npmjs.com/package/@rollup/pluginutils#attachScopes" rel="noreferrer noopener">funkcja <code>attachScopes()</code> z pakietu <code>@rollup/pluginutils</code></a>, która pozwala sprawdzić, czy dana nazwa zmiennej jest z zasięgu globalnego czy lokalnego. A że importy <em>muszą</em> być globalne, to pozwoli to wykryć potencjalne kolizje.</li>
<li>Wypadałoby też obsługiwać inne rodzaje argumentów, np. zmienne. Oczywiście nie wszystkie się&nbsp;da, ale jeśli zmienna zawiera choćby liczbę, to powinno to być możliwe. Problemem jest trzymanie informacji o tym, gdzie jest która zmienna, żeby móc szybko odczytać jej wartość.</li>
<li>Fajnie byłoby wspierać bardziej “egzotyczne” wyniki, jak np. bufory czy obiekty odpowiedzi generowane przez <code>fetch()</code>. Ale to już&nbsp;wymaga niestandardowej serializacji.</li>
</ul>
<p>Obecne rozwiązanie, po lekkim refactorze, można <a href="https://github.com/Comandeer/rollup-plugin-macros" rel="noreferrer noopener">znaleźć na GitHubie</a>. Być może komuś się do czegoś przyda. Ja, w każdym razie, miałem przyjemność w trakcie tworzenia tego kodu. A to chyba najważniejsze.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Tworzymy własny bundler typów</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow" rel="alternate" type="text/html"/>
			<published>2022-10-29T17:00:00.000Z</published>
			<updated>2022-10-29T17:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow</id>
			
				<summary><![CDATA[Krótki poradnik, jak stworzyć narzędzie, które bundle'uje TS-owe deklaracj typów.]]></summary>
			
			<content type="html"><![CDATA[<p>Nieco ponad rok temu opisałem <a href="https://blog.comandeer.pl/tworzymy-wlasny-bundler.html">proces tworzenia prymitywnego bundlera</a>. Nie tak dawno zacząłem się&nbsp;zastanawiać, czy dałoby się go w prosty sposób dostosować do bundle’owania plików z <a href="https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html" rel="noreferrer noopener">definicjami typów TS-a</a>. A że to wciąż jest <a href="https://github.com/Microsoft/TypeScript/issues/4433#issuecomment-1183981387" rel="noreferrer noopener">faktyczny problem</a>, postanowiłem to sprawdzić.</p>
<h2 id="teoria"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#teoria">Teoria</a></h2>
<p>W teorii pliki <code>.d.ts</code> wyglądają bardzo podobnie do zwykłych modułów ES. Definicje typów są podzielone na pliki, które odzwierciedlają podział plików źródłowych aplikacji. Dla przykładu, jeśli mamy takie pliki z kodem:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>src/</span></span>
<span class="line"><span>|- index.ts</span></span>
<span class="line"><span>|- tools.ts</span></span>
<span class="line"><span>|- SomeClass.ts</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>to TS wygeneruje nam takie pliki z definicjami typów:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>types/</span></span>
<span class="line"><span>|- index.d.ts</span></span>
<span class="line"><span>|- tools.d.ts</span></span>
<span class="line"><span>|- SomeClass.d.ts</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Jeśli przyjrzymy się <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/lodash/assignWith.d.ts" rel="noreferrer noopener">losowemu plikowi <code>.d.ts</code></a> (ten jest akurat dla <a href="https://lodash.com/docs/4.17.15#assignWith" rel="noreferrer noopener">lodashowej funkcji <code>assignWith()</code></a>), zauważymy znajomą składnię importów i eksportów. Więc z perspektywy bundlera zmienia się na dobrą sprawę głównie rozszerzenie pliku i pojawia się nieco inna składnia (TS zamiast czystego JS-a). Niemniej zdecydowana większość logiki bundlera powinna być możliwa do ponownego użycia bez żadnych zmian.</p>
<h2 id="przykład"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#przykład">Przykład</a></h2>
<p>Stwórzmy sobie więc przykład, który będziemy chcieli bundle’ować. Składać się on będzie z trzech plików:</p>
<ul>
<li><code>index.d.ts</code>,</li>
<li><code>Fixture.d.ts</code>,</li>
<li><code>Test.d.ts</code>.</li>
</ul>
<p>Główny plik, <code>index.d.ts</code>, będzie eksportował wszystkie pozostałe typy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Test } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF">   './Test'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Test }; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF">   './Fixture'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Żeby było ciekawie, zastosowałem tutaj dwie metody eksportu – import (1) a następnie eksport (2) oraz eksport połączony z importem (3).</p>
<p>Plik <code>Fixture.d.ts</code> zawiera interfejs <code>Fixture</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">interface</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Fixture</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">	name</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">	path</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture }; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Najpierw definiuje on interfejs (1) a następnie go eksportuje (2).</p>
<p>Z kolei plik <code>Test.d.ts</code> eksportuje interfejs <code>Test</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './Fixture'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> interface</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Test</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	readonly</span><span style="color:#E36209;--shiki-dark:#FFAB70"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createFixture</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Fixture</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wykorzystuje on interfejs <code>Fixture</code> (1), który importuje z pliku <code>Fixture.d.ts</code> (2). Natomiast eksport jest tutaj bezpośrednio połączony z deklaracją interfejsu (3).</p>
<p>Spróbujmy zatem zbundle’ować te trzy pliki razem!</p>
<h2 id="obsługa-składni-typescriptu"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#obsługa-składni-typescriptu">Obsługa składni TypeScriptu</a></h2>
<p><a href="https://babeljs.io/" rel="noreferrer noopener">Babel</a> jest parserem JS-a, więc nie ma wbudowanej obsługi składni TS-a. Niemniej istnieje <a href="https://babeljs.io/docs/en/babel-plugin-transform-typescript" rel="noreferrer noopener">oficjalny plugin</a>, który taką&nbsp;obsługę dodaje. Wystarczy go zainstalować:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Shell</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @babel/plugin-transform-typescript</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>a następnie dołączyć do naszego parsera w bundlerze:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> processModule</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">isMain</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    […]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ast</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		sourceType: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'module'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		plugins: [ </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			[</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				'typescript'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				{</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					dts: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    […]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Do parsera dodajemy opcję <code>plugins</code> (1), która pobiera tablicę pluginów  wraz z opcjami dla nich. Dodajemy do niej plugin <code>typescript</code> (2) wraz z <a href="https://babeljs.io/docs/en/babel-plugin-transform-typescript#dts" rel="noreferrer noopener">opcją <code>dts</code></a> ustawioną na <code>true</code> (3), która pozwala na parsowanie właśnie plików <code>.d.ts</code>.</p>
<p>Yay, nasz bundler JS-a właśnie stał się bundlerem plików <code>.d.ts</code>!</p>
<h2 id="ścieżki-do-plików-bez-rozszerzeń"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#ścieżki-do-plików-bez-rozszerzeń">Ścieżki do plików bez rozszerzeń</a></h2>
<p>I choć nasz bundler już w teorii powinien radzić&nbsp;sobie z plikami <code>.d.ts</code>, to TS ma kilka przypadłości składniowych, które uniemożliwiają mu sensowne działanie. Jedną z nich jest omijanie rozszerzeń plików w importach, np:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './Fixture'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tak naprawdę import odbywa się z pliku <code>Fixture.d.ts</code>, nie zaś – <code>Fixture</code>. Musimy wziąć&nbsp;na to poprawkę i przygotować prostą funkcję, która będzie nam zamieniać ścieżki z importów na poprawne ścieżki do plików:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createFilePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">importSpecifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">importSpecifier.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">endsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.d.ts'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> importSpecifier</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }.d.ts`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> importSpecifier; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Sprawdzamy, czy ścieżka kończy się rozszerzeniem <code>.d.ts</code> (1) i jeśli nie, to po prostu je dodajemy i zwracamy tak zmodyfikowaną&nbsp;ścieżką (2). W innym wypadku zwracamy oryginalną ścieżkę (3).</p>
<p>Teraz wypada dodać tę zmianę do kodu bundlera. Należy podmienić <a href="https://github.com/Comandeer/simple-bundler-example/blob/e906ab8821934ab384e0731a70043d53954dac7f/index.js#L40-L41" rel="noreferrer noopener">dwie linijki odpowiadające za wczytanie importowanego pliku</a> na poniższy kod:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> importRelativePath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createFilePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.source.value );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> depPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( dir, importRelativePath );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">modules.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#6F42C1;--shiki-dark:#B392F0">processModule</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( depPath ) );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>A że tę logikę będziemy wykorzystywać też&nbsp;w innym miejscu (<a href="https://www.youtube.com/watch?v=vQTp8Ozj1JQ" rel="noreferrer noopener">spoilers…</a>), to wyciągnijmy sobie ją od razu do osobnej funkcji, <code>processImport()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> processImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">node</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">dir</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">modules</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> importRelativePath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createFilePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.source.value );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> depPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( dir, importRelativePath );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	modules.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#6F42C1;--shiki-dark:#B392F0">processModule</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( depPath ) );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Przekazywane parametry to:</p>
<ul>
<li><code>node</code> – czyli węzeł AST z importem,</li>
<li><code>dir</code> – katalog pliku importującego (wzięty z funkcji <code>processModule()</code>),</li>
<li><code>modules</code> – tablica modułów (wzięta z funkcji <code>processModule()</code>).</li>
</ul>
<h2 id="lepsza-obsługa-eksportów"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#lepsza-obsługa-eksportów">Lepsza obsługa eksportów</a></h2>
<p>Pliki z definicjami typów raczej eksportują typy, więc byłoby miło, gdyby nasz bundler nie wycinał eksportów – ale tylko w głównym pliku (czyli tym, od którego zaczynamy bundle’owanie), bo inaczej dostaniemy niepoprawny składniowo plik, np:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> interface</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Test</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	[…]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Test };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Potrzebujemy więc sposobu, aby rozpoznawać, czy aktualnie obsługiwany plik jest tym głównym, czy nie. W tym celu wystarczy dodać parametr <code>isMain</code> do <code>processModule()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> processModule</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">isMain</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	[…]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W chwili, gdy będziemy zaczynać całe bundle’owanie, ustawimy go na <code>true</code>, a we wszystkich innych przypadkach – na <code>false</code> (lub całkowicie pominiemy i pozwolimy przyjąć mu domyślną wartość, czyli właśnie <code>false</code>).</p>
<p>Jednak proste wycinanie wszystkich eksportów z importowanych plików nie zadziała, ponieważ wytnie też konstrukcje typu <code>export interface Test {}</code>, w których deklaracja jest bezpośrednio w eksporcie. Z tego też powodu trzeba zastąpić <a href="https://github.com/Comandeer/simple-bundler-example/blob/e906ab8821934ab384e0731a70043d53954dac7f/index.js#L33" rel="noreferrer noopener">usuwanie eksportu</a> funkcją <code>handleExport()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> handleExport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#E36209;--shiki-dark:#FFAB70">isMain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">dir</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">modules</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> node</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path.node;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.source ) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		processImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node, dir, modules );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( isMain </span><span style="color:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.source ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">exportNamedDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.declaration, node.specifiers ) );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( isMain ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.declaration ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.declaration );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">remove</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Trochę się tu dzieje, więc przyjrzyjmy się po kolei poszczególnym fragmentom. Na sam początek jest obsługa sytuacji, w których eksport jest połączony z importem (<code>export { Something } from './file'</code>):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> node</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path.node; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.source ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	processImport</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node, dir, modules ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku pobieramy sobie węzeł eksportu do zmiennej (1), następnie sprawdzamy, czy ma ustawioną własność <code>source</code> (2). To właśnie ona wskazuje na plik, z którego eksportujemy. Jeśli tak, odpalamy na tym eksporcie opisaną już wcześniej funkcję <code>processImport()</code> (3). Wszystkie potrzebne parametry dostajemy z zewnątrz, z funkcji <code>processModule()</code>.</p>
<p>Następny fragment dodaje dodatkową logikę dla takich eksportów w głównym pliku. Jest to spowodowane tym, że musimy w nich zamienić eksport z zewnętrznego pliku na eksport lokalnego typu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">TypeScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './Fixture'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// trzeba zmienić na:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Fixture };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym celu używamy <code>path.replaceWith()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( isMain </span><span style="color:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> node.source ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">exportNamedDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.declaration, node.specifiers ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja <code>exportNamedDeclaration()</code> (1) pochodzi z <a href="https://babeljs.io/docs/en/babel-types" rel="noreferrer noopener">pakietu <code>@babel/types</code></a> i służy do tworzenia nowych deklaracji nazwanych eksportów. Przekazujemy do niej dane ze starego eksportu, dzięki czemu powstaje taki sam eksport, ale już&nbsp;bez informacji o zewnętrznym pliku. Z kolei <code>return</code> (2) pozwala zakończyć obsługę tego eksportu w tym miejscu, co pozwala zmniejszyć liczbę zagłębień i <code>else</code>-ów w reszcie funkcji <code>handleExport()</code>.</p>
<p>To jest też cała logika dla głównego pliku, więc jeśli w nim jesteśmy, teraz jest pora, by wyjść:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( isMain ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Następny fragment dotyczy obsługi eksportów połączonych z deklaracją (<code>export interface Test {}</code>) w importowanych plikach (w głównym takich eksportów nie ruszamy, bo i nie ma po co – główny plik powinien bez przeszkód eksportować):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( node.declaration ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( node.declaration ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Żeby wykryć taki eksport, sprawdzamy, czy zawiera deklarację (1). Jeśli tak, podmieniamy eksport na tę deklarację (2) i wychodzimy z funkcji <code>handleExport()</code> (3).</p>
<p>I w końcu, gdy mamy do czynienia z jakimkolwiek innym rodzajem eksportu, najzwyczajniej w świecie go usuwamy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">remove</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>To pozwoli pozbyć się z importowanych plików konstrukcji typu <code>export { Fixture }</code>.</p>
<p>I to tyle, stworzyliśmy prymitywny bundler typów!</p>
<h2 id="możliwe-ścieżki-rozwoju"><a class="header-anchor" href="https://blog.comandeer.pl/tworzymy-wlasny-bundler-typow#możliwe-ścieżki-rozwoju">Możliwe ścieżki rozwoju</a></h2>
<p>Podobnie do “normalnego” bundlera, który był dość prymitywny, tak i bundler typów jest mocno prymitywny i radzi sobie tylko z najprostszymi konstrukcjami. Istnieje zatem szereg możliwych usprawnień, np:</p>
<ul>
<li>obsługa aliasów w eksportach – często eksporty zawierają aliasy typu <code>export { default as someName } from './File';</code> i obecnie bundler całkowicie sobie nie radzi z takimi aliasami (zostawia je bez zmian),</li>
<li>obsługa składni <code>export = SomeThing;</code> – obecnie ta składnia działa częściowo; po prostu nie jest traktowana jako eksport, więc jest zostawiana bez zmian, co powinno działać w głównym pliku, ale już nie w importowanych,</li>
<li>dodanie tree shakingu – wiedząc, jakich typów potrzebujemy, możemy importować tylko te potrzebne z poszczególnych plików <code>.d.ts</code>,</li>
<li>dodanie zabezpieczenia przed konfliktami w importach – niektóre importowane typy mogą mieć takie same nazwy (np. <code>Node</code> z modułu obsługującego HTML i <code>Node</code> z modułu obsługującego SVG), więc warto byłoby się przed tym zabezpieczyć, choćby generując unikatowe nazwy dla wszystkich niepublicznych typów.</li>
</ul>
<p>To oczywiście nie wszystkie możliwości, jedynie kilka luźnych propozycji. W żadnym razie bundler, jaki stworzyłem, nie jest produkcyjny i przy każdym bardziej zaawansowanym pliku <code>.d.ts</code> wyglebi się na pierwszej nierówności. Do poważnych zastosowań wypadałoby wybrać coś&nbsp;bardziej sprawdzonego, np. <a href="https://github.com/Swatinem/rollup-plugin-dts" rel="noreferrer noopener"><code>rollup-plugin-dts</code></a>.</p>
<p><a href="https://github.com/Comandeer/simple-type-bundler-example" rel="noreferrer noopener">Bundler jest dostępny na GitHubie</a>.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Bramkarz na urlopie</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/bramkarz-na-urlopie" rel="alternate" type="text/html"/>
			<published>2022-09-18T17:13:00.000Z</published>
			<updated>2022-09-18T17:13:00.000Z</updated>
			<id>https://blog.comandeer.pl/bramkarz-na-urlopie</id>
			
				<summary><![CDATA[Wyjaśnienie, czemu projekt Bramkarz został zawieszony.]]></summary>
			
			<content type="html"><![CDATA[<p>Byłem zmuszony podjąć decyzję o zakończeniu projektu <a href="https://github.com/Comandeer/bramkarz" rel="noreferrer noopener">Bramkarz</a>, który rozpocząłem <a href="https://blog.comandeer.pl/bramkarz.html">jakieś 2 miesiące temu</a>. W tym wpisie pokrótce wyjaśnię dlaczego.</p>
<h2 id="dziurawe-zabezpieczenie"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz-na-urlopie#dziurawe-zabezpieczenie">Dziurawe zabezpieczenie</a></h2>
<p>Bramkarz był w założeniu narzędziem pokroju <a href="https://github.com/yaakov123/hagana" rel="noreferrer noopener">Hagany</a>, ale dla projektów, które wykorzystywały <a href="https://nodejs.org/api/esm.html" rel="noreferrer noopener">natywne moduły ES</a> zamiast, tradycyjnego dla Node.js, <a href="https://nodejs.org/api/modules.html" rel="noreferrer noopener">systemu modułów CJS</a>. To wymuszało wykorzystanie eksperymentalnego <a href="https://nodejs.org/api/esm.html#loaders" rel="noreferrer noopener">API loaderów</a>. I chociaż całość była dość toporna z powodu ograniczeń składni ESM (m.in. brak dynamicznych eksportów), to działało to nadspodziewanie dobrze.<br>
Tylko że był jeden mały problem: tego typu zabezpieczenie można było banalnie prosto obejść. Wystarczyło stworzyć plik JS z rozszerzeniem <code>.cjs</code>. Dla Node’a oznacza to, że w tym pliku znajduje się kod w składni CJS. Czyli taki, który jest wczytywany przy pomocy <code>require()</code>, nie zaś – <code>import</code>. A więc taki, który omija całe zabezpieczenie dodane przez Bramkarza przy pomocy loadera ESM…</p>
<h2 id="próba-ratowania-sytuacji"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz-na-urlopie#próba-ratowania-sytuacji">Próba ratowania sytuacji</a></h2>
<p>Na szczęście Node.js posiada <a href="https://nodejs.org/api/cli.html#-r---require-module" rel="noreferrer noopener">flagę <code>--require</code></a>, która pozwala wczytać na start moduł podany jako wartość tej flagi. W teorii zatem wystarczyłoby przygotować moduł CJS nadpisujący operacje na plikach i dorzucić flagę <code>--require</code> do naszego skryptu odpalającego Bramkarza.</p>
<p>Tylko tutaj pojawia się pierwszy z problemów: Bramkarz korzysta z <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await" rel="noreferrer noopener">tzw. top-level <code>await</code></a> do <a href="https://github.com/Comandeer/bramkarz/blob/9974ca7fd223935b1ef295dae1f3883cbd99578f/src/overrides/fs.js#L8" rel="noreferrer noopener">wczytania konfiguracji</a>. W ESM jest to możliwe, ponieważ moduły są wczytywane asynchronicznie – Node.js spokojnie zaczeka przy wczytywaniu, aż dany moduł się&nbsp;wykona. <a href="https://github.com/nodejs/node/issues/21267" rel="noreferrer noopener">W CJS jest inaczej</a> – <code>require()</code> jest synchroniczne. To znaczy, że nasz moduł nadpisujący zostanie tak naprawdę wczytany przed tym, jak zdąży cokolwiek nadpisać. Innymi słowy: ktoś może skorzystać z operacji na plikach, <em>zanim</em> te zostaną podmienione na zabezpieczone wersje. Problem można zobaczyć choćby na <a href="https://codesandbox.io/s/restless-star-ncwkkv?file=/package.json" rel="noreferrer noopener">prostym przykładzie na CodeSandboxie</a>. Odpalenie tej aplikacji da nam taki output:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>index.js loaded</span></span>
<span class="line"><span>async initialized</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Główna aplikacja odpala się <strong>przed</strong> wykonaniem się kodu wewnątrz wczytywanego modułu. W ESM przed taką sytuacją&nbsp;chroni właśnie top-level <code>await</code>.</p>
<p>Niemniej i na to istnieją haki – niekoniecznie działające stuprocentowo dobrze, ale <em>działające</em>.</p>
<figure class="figure"><a class="figure__link" href="/assets/images/bramkarz-na-urlopie/meme-556w.avif"><picture><source type="image/avif" srcset="/assets/images/bramkarz-na-urlopie/meme-440w.avif 440w, /assets/images/bramkarz-na-urlopie/meme-556w.avif 556w" sizes="90vw"><source type="image/webp" srcset="/assets/images/bramkarz-na-urlopie/meme-440w.webp 440w, /assets/images/bramkarz-na-urlopie/meme-556w.webp 556w" sizes="90vw"><source type="image/jpeg" srcset="/assets/images/bramkarz-na-urlopie/meme-440w.jpeg 440w, /assets/images/bramkarz-na-urlopie/meme-556w.jpeg 556w" sizes="90vw"><img src="/assets/images/bramkarz-na-urlopie/meme-556w.jpeg" width="556" height="449" style="aspect-ratio: 556 / 449" alt="–You are without doubt the worst hack I've ever heard of! –But you have heard of me" loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Kliknij obrazek, aby go powiększyć</p></figcaption></figure>
<p>Istnieje w Node.js <a href="https://nodejs.org/api/module.html" rel="noreferrer noopener">moduł <code>node:module</code></a>, który zawiera całą logikę obsługi modułów CJS. Wśród niej jest (nieudokumentowana) metoda <code>Module._load</code>, służąca właśnie do wczytywania modułów. Nadpisanie jej w teorii pozwala dodać wsparcie dla asynchronicznego <code>require()</code>. A przynajmniej takiego, które poczeka na wczytanie się konfiguracji Bramkarza:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Module</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'node:module'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> originalLoad</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Module._load; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> somePromise</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    setTimeout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">Module.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">_load</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#E36209;--shiki-dark:#FFAB70">args</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    somePromise.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">then</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> originalLoad</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">args ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    } );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na sam początek wczytujemy moduł <code>node:module</code> (1). Następnie zapisujemy sobie oryginalną wersję metody <code>Module#_load()</code> do zmiennej (2). Potem tworzymy obietnicę <code>somePromise</code> (3), która będzie emulować wczytywanie się konfiguracji. Jedyne, co robi, to odczekanie sekundy (4). Na samym końcu nadpisujemy <code>Module#_load()</code> (5). Nowa wersja czeka, aż obietnica <code>somePromise</code> zostanie rozwiązana, co można zrobić przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then" rel="noreferrer noopener"><code>#then()</code></a> (6). Po rozwiązaniu obietnicy odpalamy oryginalną metodę <code>Module#load()</code> (7). Taki “loader” można dodać do naszej aplikacji właśnie przy pomocy flagi <code>--require</code>.</p>
<p><a href="https://codesandbox.io/s/stupefied-heisenberg-y3qqx8?file=/loader.js" rel="noreferrer noopener">Ten przykład również można zobaczyć na CodeSandboxie</a>.</p>
<div class="note" role="note" aria-labelledby="note-44"><p class="note__label" id="note-44">Dygresja</p><div class="note__content"><p>Na CodeSandboxie rezultat jest widoczny najlepiej po odpaleniu nowego terminalu i wpisaniu w nim komendy <code>npm start</code>. Przykłady można też ściągnąć jako plik <code>.zip</code> i odpalić lokalnie.</p></div></div>
<p>Tylko że tutaj trafiłem na kolejny problem: mój projekt ma zależności. Większość z nich stworzył Sindre Sorhus, który <a href="https://scribe.rip/sindre-sorhus/hello-modules-d1010b4e777b" rel="noreferrer noopener">porzucił&nbsp;wsparcie dla CJS jakiś czas temu</a>. I jak moduły CJS da się&nbsp;wczytać wewnątrz ESM, tak w drugą stronę już nie bardzo. Owszem, są na to odpowiednie haki, np. używanie <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import" rel="noreferrer noopener">dynamicznych importów</a>, ale to rodzi kolejne problemy (np. <a href="https://rollupjs.org/guide/en/#renderdynamicimport" rel="noreferrer noopener">konieczność zmiany konfiguracji Rollupa</a>). I to był ten moment, gdy stwierdziłem, że odpuszczam. Zbyt dużo problemów jak na projekt, który jest zwykłym eksperymentem.</p>
<h2 id="i-to-by-było-na-tyle"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz-na-urlopie#i-to-by-było-na-tyle">I to by było na tyle</a></h2>
<p>Szkoda mi tego projektu, bo bardzo podobała mi się idea zrobienia narzędzia, które pozwalałoby na wprowadzenie do Node’a czegoś na wzór systemu uprawnień. Niemniej trudności, które napotkałem, skutecznie mnie zniechęciły. A już nawet wygenerowałem ładne logo dla projektu przy użyciu <a href="https://openai.com/dall-e-2/" rel="noreferrer noopener">Dall·e 2</a>:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/bramkarz-na-urlopie/logo-1024w.avif"><picture><source type="image/avif" srcset="/assets/images/bramkarz-na-urlopie/logo-440w.avif 440w, /assets/images/bramkarz-na-urlopie/logo-880w.avif 880w, /assets/images/bramkarz-na-urlopie/logo-1024w.avif 1024w" sizes="90vw"><source type="image/webp" srcset="/assets/images/bramkarz-na-urlopie/logo-440w.webp 440w, /assets/images/bramkarz-na-urlopie/logo-880w.webp 880w, /assets/images/bramkarz-na-urlopie/logo-1024w.webp 1024w" sizes="90vw"><source type="image/png" srcset="/assets/images/bramkarz-na-urlopie/logo-440w.png 440w, /assets/images/bramkarz-na-urlopie/logo-880w.png 880w, /assets/images/bramkarz-na-urlopie/logo-1024w.png 1024w" sizes="90vw"><img src="/assets/images/bramkarz-na-urlopie/logo-1024w.png" width="1024" height="1024" style="aspect-ratio: 1024 / 1024" alt="Bramkarz w eleganckim garniturze przed wejściem do klubu nocnego" loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Kliknij obrazek, aby go powiększyć</p></figcaption></figure>
<p>No cóż, może Bramkarz doczeka się lepszych czasów, np. Node.js kiedyś porzuci wsparcie dla CJS. Na razie jednak Bramkarz zostaje wysłany na urlop.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Bramkarz</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/bramkarz" rel="alternate" type="text/html"/>
			<published>2022-07-31T11:45:00.000Z</published>
			<updated>2022-07-31T11:45:00.000Z</updated>
			<id>https://blog.comandeer.pl/bramkarz</id>
			
				<summary><![CDATA[Eksperyment z tworzeniem narzędzia nadzorującego dostęp do plików w Node.js.]]></summary>
			
			<content type="html"><![CDATA[<p>Ostatnio natrafiłem na projekt <a href="https://github.com/yaakov123/hagana" rel="noreferrer noopener">Hagana</a>, który oferuje ochronę w trakcie wykonywania skryptu Node.js. Polega ona na blokowaniu operacji sieciowych oraz operacji na plikach poza katalogiem projektu. Postanowiłem zatem sprawdzić, jak to dokładnie działa pod spodem.</p>
<h2 id="proxy-czyli-stary-znajomy"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz#proxy-czyli-stary-znajomy">Proxy, czyli stary znajomy</a></h2>
<p>Myślałem, że w środku znajdę coś naprawdę ciekawego… i trochę się zawiodłem. Zobaczyłem bowiem rzecz, którą bawię się od dłuższego czasu i którą już nieraz opisywałem tutaj na blogu. Jedyna różnica polega na tym, że Hagana wykorzystuje ją w innych celach.</p>
<p>A o jakiej rzeczy tak w zasadzie mówię? O proxy, przy pomocy którego nie tak dawno <a href="https://blog.comandeer.pl/mutowalna-niemutowalnosc.html">odtwarzałem wycinek Immera</a>. Okazuje się, że ten mechanizm można także wykorzystać, żeby lepiej zabezpieczyć swoją aplikację w Node.js. Trzonem w Haganie są bowiem tzw. <a href="https://github.com/yaakov123/hagana/tree/5e9c881cac1171f7e62abee163ff4c6d1747385c/src/overrides" rel="noreferrer noopener"><i lang="en">overrides</i> – funkcje nadpisujące natywne mechanizmy Node’a</a>. Nadpisaniu ulegają m.in. <a href="https://github.com/yaakov123/hagana/blob/5e9c881cac1171f7e62abee163ff4c6d1747385c/src/overrides/fileSystem.ts" rel="noreferrer noopener">funkcje odpowiedzialne za odczyt i zapis do plików</a>. Każda taka funkcja jest opakowywana w proxy, które przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply" rel="noreferrer noopener">pułapki <code>apply</code></a> wykonuje dodatkową walidację przed faktyczną operacją na plikach. Dzięki temu możliwe jest zabronienie operacji na plikach, które znajdują się poza katalogiem projektu (czyli nici z podkradania <code>/etc/passwd</code>…).</p>
<p>Funkcje odpowiadające za nadpisanie poszczególnych mechanizmów (obsługi plików, wątków roboczych itd.) są odpalane w chwili importu Hagany do naszej aplikacji. I to w sumie tyle – nie ma tutaj nic przesadnie skomplikowanego, a działa to całkiem dobrze.</p>
<h2 id="esm-czyli-zaczynają-się-problemy"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz#esm-czyli-zaczynają-się-problemy">ESM, czyli zaczynają się problemy</a></h2>
<p>Niemniej pierwsze, co rzuciło mi się w oczy, to fakt, że Hagana była raczej tworzona z myślą o modułach CJS (czyli tych <em>starych</em>, z <code>require()</code>). Intuicja podpowiadała mi, że sposób nadpisywania poszczególnych funkcji nie będzie działał (lub będzie działał częściowo) w przypadku <em>nowych</em> modułów (czyli tych z <code>import</code>).</p>
<p>Najłatwiej wyjaśnić to na prostym przykładzie. Wyobraźmy sobie, że mamy plik <code>fs.mjs</code>, który imituje <a href="https://nodejs.org/api/fs.html" rel="noreferrer noopener">natywny moduł <code>fs</code> z Node.js</a>. Dla uproszczenia będzie on zawierał tylko jedną funkcję, <code>readFile()</code>. W przypadku natywnego modułu <code>fs</code> funkcja taka dostępna byłaby na co najmniej dwa sposoby:</p>
<ol>
<li>
<p>jako własność domyślnego eksportu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'jakaś/ścieżka'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, callback );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure></li>
<li>
<p>jako osobny import:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { readFile } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'jakaś/ścieżka'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, callback );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure></li>
</ol>
<p>Stwórzmy zatem moduł, który by działał w taki sposób:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {} </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fs</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	readFile </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { readFile }; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku tworzymy funkcję&nbsp;<code>readFile()</code> (1). Następnie tworzymy obiekt <code>fs</code> (2), w którym tworzymy własność&nbsp;<code>readFile</code> zawierającą naszą funkcję (3). Ten obiekt będzie naszym domyślnym eksportem (4). Oprócz tego eksportujemy osobno samą funkcję <code>readFile()</code> (5).</p>
<p>Stwórzmy jeszcze moduł, który będzie próbował nadpisać naszą funkcję <code>readFile()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs, { readFile } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './fs.mjs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( fs.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> readFile ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// true – 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readFile</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {}; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( fs.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> readFile ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// false – 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">readFile</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {}; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// error – 5</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na samym początku importujemy zarówno domyślny eksport, jak i bezpośrednio funkcję <code>readFile()</code> (1). Sprawdzamy sobie, czy <code>fs#readFile()</code> to ta sama funkcja co <code>readFile()</code> (2). I faktycznie, jest to ta sama funkcja. Następnie nadpisujemy <code>fs#readFile()</code> nową funkcją (3) i ponawiamy sprawdzenie (4). Tym razem otrzymujemy już fałsz. Natomiast próba nadpisania bezpośrednio <code>readFile()</code> (5) kończy się błędem.</p>
<p>A to dlatego, że w uproszczeniu importy można traktować jako zmienne zadeklarowane przy pomocy <code>const</code>. Z tego też powodu można mutować zaimportowany obiekt <code>fs</code>, ale już nie można nadpisać importowanych wartości. Innymi słowy: w przypadku ESM użytkownik będzie zabezpieczony wyłącznie, gdy będzie używał <code>fs#readFile()</code>, ale gdy zaimportuje <code>readFile()</code> bezpośrednio, to ominie całe nasze zabezpieczenie. A to brzmi jak naprawdę poważny problem.</p>
<p>Czemu jednak działa to w CJS? Ponieważ tam eksportowany jest po prostu obiekt z poszczególnymi funkcjami. Nawet zrobienie</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'node:fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>oznacza jedynie <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment" rel="noreferrer noopener">destrukturyzację</a> tego obiektu. ESM są pod tym względem, niestety, bardziej kłopotliwe.</p>
<h2 id="bramkarz-czyli-próba-obejścia"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz#bramkarz-czyli-próba-obejścia">Bramkarz, czyli próba obejścia</a></h2>
<p>Zacząłem się zastanawiać, czy jest jakiś sposób, aby być w stanie nadpisać obydwa sposoby importowania funkcji obsługujących pliki w przypadku, gdy aplikacja korzysta z natywnej obsługi ESM w Node.js. Jedyną rzeczą, jaka przychodziła mi do głowy, były <a href="https://nodejs.org/api/esm.html#loaders" rel="noreferrer noopener">niestandardowe loadery</a>. Ich zamysł opisałem już kiedyś na blogu w <a href="https://blog.comandeer.pl/html-w-node.html">artykule poświęconym wczytywaniu plików <code>.html</code> bezpośrednio w Node.js</a>. Co prawda, od tamtego czasu dość znacząco zmieniła się składnia rozwiązania, ale podstawy teoretyczne pozostały te same: dzięki niestandardowym loaderom możemy podstawić dowolny kod w miejsce konkretnego modułu.</p>
<p>Taki niestandardowy loader zazwyczaj składa się z dwóch zasadniczych części – asynchronicznej funkcji <code>resolve()</code>, która przerabia nazwy modułów (takie jak <code>node:fs</code> czy <code>@comandeer/rollup-lib-bundler</code>) na URL-e, oraz asynchronicznej funkcji <code>load()</code>, która zajmuje się faktycznym wczytywaniem modułów. Całość jest stosunkowo prosta i w zupełności wystarczająca do wykonania tego, co chciałem.</p>
<p>Zamysł był prosty: funkcja <code>resolve()</code> będzie otrzymywać informacje o tym, kiedy program próbuje załadować moduł <code>node:fs</code> i na ich podstawie będzie decydować, czy należy wczytać nasz podmieniony moduł. Loader, oprócz samej nazwy/URL-a modułu, dostaje także dodatkowe informacje, w tym o rodzicu danego modułu – czyli o module, który wczytał aktualnie importowany moduł. Postanowiłem to wykorzystać. W chwili, gdy program chciał wczytać moduł <code>node:fs</code>, podmieniałem go na adres tego nadpisującego. Oczywiście wewnątrz tego nadpisującego modułu musiałem importować oryginalne <code>node:fs</code>. Dzięki informacji o rodzicu modułu mogłem ominąć problem wywołania nieskończonej pętli importów. Nie podmieniałem bowiem <code>node:fs</code> w momencie, gdy był importowany ze środka mojego nadpisującego modułu.</p>
<p>Podsumowując:</p>
<ol>
<li>Funkcja <code>resolve()</code> sprawdza, czy program nie próbuje importować <code>node:fs</code>.</li>
<li>Jeśli tak, sprawdzany jest rodzic modułu i jeśli jest inny od mojego nadpisującego modułu (dla ułatwienia nazwijmy go <code>bramkarz:fs</code>), URL <code>node:fs</code> podmieniany jest na URL modułu <code>bramkarz:fs</code>.</li>
<li>Moduł <code>bramkarz:fs</code> jest wczytywany przez domyślny loader Node’a.</li>
<li>Moduł <code>bramkarz:fs</code> wczytuje oryginalny moduł <code>node:fs</code>.</li>
<li>Funkcja <code>resolve()</code> ignoruje ten import, bo widzi, że rodzicem jest <code>bramkarz:fs</code>.</li>
<li>Mooduł <code>bramkarz:fs</code> zwraca funkcje operujące na plikach, które są opakowane w proxy sprawdzające, czy podana ścieżka do pliku powinna być dostępna dla programu.</li>
</ol>
<p>Brzmi to dość skomplikowanie i na pierwszy rzut oka jest nieco przekombinowane, ale na tę&nbsp;chwilę nie przychodzi mi do głowy żaden inny sposób, w jaki można by to rozwiązać.</p>
<p>A skoro już siadłem do zabawy nad tym, stwierdziłem, że może spróbuję napisać jakieś sensowne narzędzie. Tak narodził się <a href="https://github.com/Comandeer/bramkarz" rel="noreferrer noopener">Bramkarz</a>. Co prawda jest na bardzo wczesnym etapie rozwoju, ale można już podpatrzeć w nim <a href="https://github.com/Comandeer/bramkarz/blob/c589cb6bdafce6f739355dbcf0efdff7b9fc3539/bin/loader.js" rel="noreferrer noopener">niestandardowy loader,</a> o którym przed chwilą pisałem. Dodatkowo doszedłem do wniosku, że bardziej przyjazne użytkownikowi będzie stworzenie prostego programu konsolowego, który się będzie odpalać zamiast Node’a (czyli <code>bramkarz .</code> zamiast <code>node .</code>). On już zadba o to, by loader był dołączany do uruchamianego programu i wykonywał w tle swoją&nbsp;robotę, niemalże niezauważalnie dla użytkownika. Natomiast sama konfiguracja została wyrzucona do osobnego pliku, <code>.bramkarzrc.json</code>.</p>
<h2 id="esm-czyli-moje-problemy-mają-problemy"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz#esm-czyli-moje-problemy-mają-problemy">ESM, czyli moje problemy mają problemy</a></h2>
<p>Chyba największym problemem (czy też – upierdliwością) jest mocno statyczna składnia ESM. W idealnym świecie nadpisanie modułu wyglądałoby tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> *</span><span style="color:#D73A49;--shiki-dark:#F97583"> as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> newFS</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">fs }; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">newFS.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newFS.default.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createProxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { ...newFS }; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importuję sobie nasz moduł (1). Następnie tworzę jego kopię (2) i nadpisuję w niej interesujące mnie rzeczy (3). Następnie używam destrukturyzacji w eksporcie, żeby wyeksportować wszystko, co jest w <code>newFS</code> (4).</p>
<p>Innymi słowy zakładałbym, że destrukturyzacja w eksporcie robiłaby to samo co kod poniżej:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { fs.</span><span style="color:#D73A49;--shiki-dark:#F97583">default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> as </span><span style="color:#D73A49;--shiki-dark:#F97583">default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { fs.readFile as readFile };</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// itd.</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W rzeczywistości ani pierwszy, ani drugi kod nie działa… Jestem więc skazany na rzeźbienie wszystkich eksportów ręcznie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> *</span><span style="color:#D73A49;--shiki-dark:#F97583"> as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> newFS</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">fs };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">newFS.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newFS.default.readFile </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createProxy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newFS;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newFS.default;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { readFile };</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tak na razie wygląda nadpisanie <code>fs</code> w Bramkarzu. Powyższy kod tworzy tak naprawdę moduł jedynie z metodą <code>readFile()</code> oraz domyślnym eksportem. Reszta pojedynczych eksportów jest stracona, dopóki nie będę miał czasu i chęci, by je na powrót dodać… A że eksporty mogą być tylko na najwyższym poziomie, bez żadnego zagnieżdżenia, to odpada nawet pętla <code>for...of</code> do przeiterowania po <code>newFS</code> i stworzenia poszczególnych eksportów. To naprawdę trzeba zrobić wszystko <em>ręcznie</em>.</p>
<h2 id="rozwój-czyli-co-dalej"><a class="header-anchor" href="https://blog.comandeer.pl/bramkarz#rozwój-czyli-co-dalej">Rozwój, czyli co dalej?</a></h2>
<p>Nie wiem, czy będę próbował rozwijać Bramkarza i czy w ogóle wypuszczę jakąkolwiek wersję na npm-ie. Na ten moment mnie mocno zmęczył, głównie ze względu na to, jak mało przyjazne wydają się narzędzia do pracy z natywnymi ESM w Node w porównaniu do modułów CJS, które dzięki transpilatorom i tak mogą wyglądać jak ESM.</p>
<p>Z drugiej strony podejście Bramkarza wydaje się nieco bezpieczniejsze niż Hagany. Loader, który działa w tle, niezależnie od programu, który ma chronić, brzmi jak coś bardziej odpornego na próby przechytrzenia, niż nadpisywanie modułów dopiero w chwili importu zabezpieczającego modułu. Dodatkowo jest to bardziej przezroczyste dla użytkownika, który po prostu odpala program i ten jest chroniony z automatu.</p>
<p>Niemniej sam pomysł takiego zabezpieczenia mi się podoba. Zdecydowanie brakuje czegoś&nbsp;takiego wbudowanego w Node.js. <a href="https://deno.land/manual/getting_started/permissions" rel="noreferrer noopener">Deno robi to zdecydowanie lepiej</a>. Może <a href="https://github.com/nodejs/security-wg/issues/791" rel="noreferrer noopener"><em>kiedyś</em> dochrapiemy się tego także w Node.js</a>. Na ten jednak moment rozwiązania pokroju Hagany czy właśnie Bramkarza wydają się wartą rozważenia namiastką.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Kręciołek!</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/kreciolek" rel="alternate" type="text/html"/>
			<published>2021-07-31T21:50:00.000Z</published>
			<updated>2021-07-31T21:50:00.000Z</updated>
			<id>https://blog.comandeer.pl/kreciolek</id>
			
				<summary><![CDATA[Krótki opis eksperymentowania z tworzeniem własnego CLI spinnera.]]></summary>
			
			<content type="html"><![CDATA[<p>Jeśli w wolnym czasie człowiek bawi się w tworzenie narzędzi uruchamianych w terminalu, to prędzej czy później stanie przed poważnym wyzwaniem – implementacją&nbsp;kręciołka, znanego też pod swoją angielską nazwą, spinnera. Przyszło zatem i mnie zmierzyć&nbsp;się z tym tematem.</p>
<h2 id="dlaczego-nie-gotowe-rozwiązania"><a class="header-anchor" href="https://blog.comandeer.pl/kreciolek#dlaczego-nie-gotowe-rozwiązania">Dlaczego nie gotowe rozwiązania?</a></h2>
<p>“Przecież pewnie jest pełno gotowych spinnerów w npm!” – zakrzyknie Co Bardziej Rozgarnięty Czytelnik. I muszę przyznać Ci, drogi Czytelniku, rację. Tak, jest ich pełno. Ale mój szybki research przekonał mnie, że większość ma mało przyjazne API, nie była aktualizowana od długiego czasu lub ogólnie wydaje się nie być rozwijana. Na szczęście istnieje <a href="https://sindresorhus.com/" rel="noreferrer noopener">Sindre Sorhus</a>, który w JS napisał już wszystko, więc także i kręciołka. Tak trafiłem na pakiet <a href="https://github.com/sindresorhus/ora" rel="noreferrer noopener"><code>ora</code></a>, który ze wszystkich znalezionych przeze mnie spinnerów miał najprzyjaźniejsze API. Nie szukając zatem dłużej, zainstalowałem i… okazało się, że z bliżej niezidentyfikowanych powodów <code>ora</code> nie działała. Spinner często się duplikował, a dodatkowo nie był widoczny w terminalu nawet po wywołaniu metody do jego usuwania. Po 1.5 godziny debugowania doszedłem do wniosku, że to przecież <em>tylko</em> spinner i nie ma sensu marnować na to większej ilości czasu.</p>
<p>Sięgnąłem zatem po opcję atomową. Wychodząc z logicznego założenia, że spinner, który pokazuje się w npm przy instalacji zależności, <em>musi</em> działać (w końcu wyświetlany jest jakieś… <strong>kilkadziesiąt milionów</strong> razy miesięcznie), poszperałem i znalazłem pakiet <a href="https://github.com/npm/gauge" rel="noreferrer noopener"><code>gauge</code></a>. I faktycznie, działa i już go wdrożyłem w swoich projektach potrzebujących kręciołka. Tylko że jest jeden problem. Tak, jak większość narzędzi wykorzystywanych w npm, tak i ten ma API równie przyjemne, co jedzenie kawałków szkła zalanych kwasem siarkowym na śniadanie. Prosty przykład: trzeba ręcznie wywoływać każdą klatkę animacji spinnera. W chwili, gdy wdrażałem spinner, był także <a href="https://github.com/npm/gauge/issues/123" rel="noreferrer noopener">problem związany z brakiem  jednej z zależności w <code>package.json</code></a>, ale widzę, że został rozwiązany kilka dni temu.</p>
<p>Dlatego doszedłem do wniosku, że sprawdzę, czy nie da się napisać jakiegoś&nbsp;prymitywnego kręciołka samodzielnie, bez potrzeby uciekania się do gotowców. I okazuje się, że jak najbardziej się&nbsp;da i nie jest to specjalnie trudne!</p>
<h2 id="kontrola-nad-terminalem"><a class="header-anchor" href="https://blog.comandeer.pl/kreciolek#kontrola-nad-terminalem">Kontrola nad terminalem</a></h2>
<p>Jednym z najpopularniejszych pakietów do pracy z terminalem jest <a href="https://github.com/chalk/chalk" rel="noreferrer noopener"><code>chalk</code></a> (stworzony przez Sindre’a Sorhusa – a jakże!). Pakiet ten pozwala na kolorowanie komunikatów wyświetlanych w terminalu. Dlaczego o nim wspominam? Bo wykorzystuje on specjalne sekwencje znaków, które terminal odczytuje jako polecenie przełączenia na odpowiedni kolor, np.</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\x1b</span><span style="color:#032F62;--shiki-dark:#9ECBFF">[31mTest'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Powyższy kod odpalony w Node.js wyświetli czerwony tekst “Test”. Odpowiedzialny za to jest kod <code>\x1b[31m</code>.  <a href="https://en.wikipedia.org/wiki/ANSI_escape_code" rel="noreferrer noopener">Kodów jest o wiele, wiele więcej</a> i oprócz kolorów pozwalają też&nbsp;na kontrolowanie m.in. pozycji kursora.</p>
<p>Jedynym problemem może być wsparcie dla tego typu sekwencji ucieczki (jak to się ładnie nazywa). Jeśli wsparcie na Linuksach czy macOS-ach jest pewne, tak problem może pojawić się na Windowsie. Z własnego doświadczenia mogę powiedzieć, że na Windowsie 10 powinno działać – zarówno w standardowym cmd, jak i w PowerShellu. Na starszych Windowsach prawdopodobnie będziemy musieli obejść&nbsp;się&nbsp;smakiem.</p>
<h2 id="ogólne-założenia-kręciołka"><a class="header-anchor" href="https://blog.comandeer.pl/kreciolek#ogólne-założenia-kręciołka">Ogólne założenia kręciołka</a></h2>
<p>Zasada działania kręciołka jest dość prosta. Jak przyjrzymy się, jak zachowuje się terminal, to zauważymy, że wypluwa informacje linia po linii. Każdy <code>console.log</code> to kolejna linia. Z kolei kręciołek “siedzi” cały czas w tej samej linii i w dodatku jest animowany. Musimy zatem znaleźć&nbsp;sposób na to, aby zablokować przechodzenie do kolejnej linii…</p>
<p>Albo podejść do sprawy z innej strony. Zablokowanie przechodzenia do kolejnej linii jest tak naprawdę równoznaczne z wyczyszczeniem aktualnej linii i zapisaniem w niej nowej treści. Zatem przy każdej klatce animacji usuwamy aktualną linię w terminalu i wstawiamy w niej nową zawartość. Można to zrobić przy pomocy odpowiednich kodów. Najpierw należy przejść na początek aktualnej linii przy pomocy <code>\r</code> (czyli znaku powrotu karetki), a następnie użyć kodu <code>\x1b[K</code>, który powoduje usunięcie zawartości linii od pozycji kursora do samego jej końca. Po usunięciu zawartości linii można następnie wstawić kolejną klatkę animacji. Cały mechanizm przypomina nieco używanie <a href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame" rel="noreferrer noopener"><code>requestAnimationFrame</code></a> w przeglądarce. Tam też jesteśmy zmuszeni do kontrolowania każdej klatki osobno.</p>
<p>Pozostaje jeszcze jeden problem: <code>console.log</code> wymusza przejście do nowej linii na końcu każdego wyświetlonego komunikatu. A tego nie chcemy. Tu na szczęście przychodzi nam z pomocą fakt, że <code>process.stdout</code> (czyli tzw. standardowe wyjście, a więc w naszym wypadku terminal) jest <a href="https://nodejs.org/api/stream.html" rel="noreferrer noopener">strumieniem</a>. Dzięki temu możemy całkowicie pominąć pośrednika w postaci <code>console</code> i wypisywać komunikaty bezpośrednio. Da nam to całkowitą kontrolę nad formatowaniem, w tym nad znakami nowej linii. A to dokładnie to, czego potrzebujemy, by upewnić się, że spinner będzie prawidłowo odświeżany.</p>
<h2 id="prototyp"><a class="header-anchor" href="https://blog.comandeer.pl/kreciolek#prototyp">Prototyp</a></h2>
<p>Napiszmy więc szybki prototyp. Stwórzmy sobie plik <code>spinner.js</code>. Pierwsze, co będziemy chcieli zrobić, to stworzyć sobie tablicę klatek ze spinnerem. W naszym wypadku będą to kreski:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> spinner</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'-'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'|'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Następnie chcemy napisać prostą funkcję, która będzie pobierała poszczególne klatki spinnera. Nazwijmy ją <code>prepareFrame</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> spinner</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	…</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentFrame </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner[ currentFrame</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#D73A49;--shiki-dark:#F97583"> %</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ]; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy sobie licznik <code>currentFrame</code> (1), który będzie nam wskazywał, która klatka animacji powinna być aktualnie wyświetlana. Następnie w dość przerażająco wyglądającej linijce (2) pobieramy potrzebną nam ramkę. Rozbijmy tę linijkę na części.</p>
<p><code>currentFrame++</code> oznacza, że wartość <code>currentFrame</code> zostanie zwiększona o 1 po wykonaniu działania obecnego w tej linijce. To oznacza, że dla pierwszego wywołania <code>prepareFrame</code> możemy wyrażenie <code>currentFrame++</code> zamienić na <code>0</code>. Z kolei <code>spinner.length</code> możemy podmienić na <code>4</code> (bo tyle mamy kresek w tablicy <code>spinner</code>). Otrzymujemy zatem mniej groźnie wyglądające działanie <code>0 % 4</code> – czyli 0. Dla kolejnego wywołania będziemy mieli <code>1 % 4</code> – czyli 1 itd. Wykorzystanie operatora <code>%</code> pozwala nam na nieresetowanie licznika, gdy ten przekroczy długość tablicy, np. <code>8 % 4</code> da nam 0, <code>9 % 4</code> – 1 itd.</p>
<p>Liczbę uzyskaną z tego działania przekazujemy jako indeks do tablicy <code>spinner</code> i zwracamy element znajdujący się pod tym indeksem. W ten sposób wyciągamy odpowiednią kreskę z tablicy.</p>
<p>Jednak sama kreska musi być też narysowana i podmieniana przez nowe co określony czas. Napiszmy zatem funkcję <code>drawFrame</code>, która będzie to robić:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> drawFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextFrame</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nextFrame );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	setTimeout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( drawFrame, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">60</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">drawFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli teraz odpalimy nasz program, zauważymy, że w nieskończoność wyświetla kreski z tablicy po kolei (program można ubić naciskając <kbd>Ctrl</kbd> + <kbd>C</kbd>, również na macOS-ie). Osiągnęliśmy to dzięki zastosowaniu <code>setTimeout</code> (1), które wywołuje rysowanie kolejnej klatki animacji (czyli dokładnie jak przy <code>requestAnimationFrame</code>!). Wykorzystujemy tutaj także funkcję <code>prepareFrame</code> do pobierania kolejnych klatek (2). Na samym końcu wywołujemy funkcję <code>drawFrame</code> (3), by rozpocząć&nbsp;animację.</p>
<p>Oczywisty problem z aktualnym podejściem polega na tym, że wykorzystuje <code>console.log</code>, przez co uzyskujemy każdą klatkę w nowej linii. Zmieńmy zatem nieco kod funkcji <code>drawFrame</code> i zastosujmy strumień:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> drawFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextFrame</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	process.stdout.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">write</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nextFrame, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		setTimeout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( drawFrame, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">60</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Pojawiła się funkcja <code>process.stdout.write</code> (1). Służy ona do wysyłania danych do strumienia. Funkcja ta przyjmuje trzy argumenty: tekst (dane), który chcemy umieścić w strumieniu, kodowanie tego tekstu oraz callback, który zostanie wywołany po umieszczeniu danych w strumieniu. W naszym wypadku po prostu kolejkujemy rysowanie kolejnej klatki (2).</p>
<div class="note" role="note" aria-labelledby="note-19"><p class="note__label" id="note-19">Dygresja</p><div class="note__content"><p>Bardzo często spinnery są wyświetlane w <code>stderr</code> (czyli strumieniu przeznaczonym na błędy). Jest to związane z niepisaną konwencją, według której <a href="https://web.archive.org/web/20201111211140/https://www.jstorimer.com/blogs/workingwithcode/7766119-when-to-use-stderr-instead-of-stdout" rel="noreferrer noopener">wszystkie diagnostyczne rzeczy powinny trafiać właśnie tam</a>.</p></div></div>
<p>Jeśli odpalimy program teraz, zauważymy, że kolejne klatki co prawda wyświetlają się w tej samej linii, ale poprzednie nie są usuwane. A to dlatego, że nie usuwamy tego, co jest w tej linii. W tym celu najlepiej będzie zmienić funkcję <code>prepareFrame</code>, by dodawała do klatki także odpowiednie sekwencje ucieczki:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> eraseLineCmd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\r\x1b</span><span style="color:#032F62;--shiki-dark:#9ECBFF">[K'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextFrame</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner[ currentFrame</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#D73A49;--shiki-dark:#F97583"> %</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ]; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> eraseLineCmd </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> nextFrame; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Teraz funkcja zwraca ramkę (1), która składa się&nbsp;z dwóch rzeczy: kodu, który przesuwa kursor na początek linii, a następnie usuwa jej zawartość (2), oraz samej ramki (3). Po odpaleniu naszego programu w takiej wersji, uzyskamy w końcu ładny kręciołek 🎉!</p>
<p>Cały kod wygląda tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> spinner</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'-'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\\</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	'|'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentFrame </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> eraseLineCmd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\r\x1b</span><span style="color:#032F62;--shiki-dark:#9ECBFF">[K'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextFrame</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner[ currentFrame</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#D73A49;--shiki-dark:#F97583"> %</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> spinner.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> eraseLineCmd </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> nextFrame;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> drawFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextFrame</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> prepareFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	process.stdout.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">write</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nextFrame, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		setTimeout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( drawFrame, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">60</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">drawFrame</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h2 id="co-dalej"><a class="header-anchor" href="https://blog.comandeer.pl/kreciolek#co-dalej">Co dalej?</a></h2>
<p>Oczywiście taki kręciołek można rozwijać na wiele sposobów. Jednym z nich jest dodanie wsparcia dla etykiet tekstowych wyświetlanych obok animowanego kręciołka. Warto także dodać wykrywanie, czy aby na pewno terminal użytkownika wspiera takie rzeczy. No i w końcu warto to wszystko otoczyć w jakieś przyjemne API, by można było kontrolować, kiedy spinner ma się pokazywać i ukrywać. Jak to wygląda (czy też będzie wyglądać) u mnie, <a href="https://github.com/Comandeer/cli-spinner" rel="noreferrer noopener">można zobaczyć na GitHubie</a>.</p>
<p>PS jakby ktoś pytał, NIH to dla mnie taki <a href="http://notinventedhe.re/" rel="noreferrer noopener">komiks internetowy</a>.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Jak działa narzędzie do code coverage?</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage" rel="alternate" type="text/html"/>
			<published>2021-01-31T22:20:00.000Z</published>
			<updated>2021-01-31T22:20:00.000Z</updated>
			<id>https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage</id>
			
				<summary><![CDATA[Krótkie spojrzenie wgłąb narzędzi do sprawdzania pokrycia kodu testami.]]></summary>
			
			<content type="html"><![CDATA[<p>Dzisiaj kontynuujemy zabawy z AST. Tym razem padło na narzędzie do code coverage!</p>
<h2 id="zasada-działania"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#zasada-działania">Zasada działania</a></h2>
<p>Zacznijmy od szybkiego wyjaśnienia, czym jest code coverage (pokrycie kodu)? To wskażnik informujący nas o tym, ile kodu faktycznie zostało <em>pokrytych</em> testami automatycznymi. Jeśli jakiś kod (np. funkcja <code>bakeCake</code>) nie został wykonany w czasie testów, to znaczy, że jest tak naprawdę nieprzetestowany (trudno przetestować ciasto bez jego wcześniejszego upieczenia!). Stąd też code coverage pozwala wyłapać tego typu sytuacje i dopisać brakujące testy.</p>
<p>Ale jak to dokładnie działa? Bodaj najpopularniejszym narzędziem do code coverage w światku JS jest <a href="https://github.com/istanbuljs/istanbuljs" rel="noreferrer noopener">IstanbulJS</a> i to właśnie jemu się przyjrzymy. Standardowo używa się go przy pomocy narzędzia CLI, <a href="https://github.com/istanbuljs/nyc" rel="noreferrer noopener"><code>nyc</code></a>, które “dokłada się” do polecenia testującego, np. <code>nyc mocha tests/*.js</code>. W ten sposób dokładamy do naszych testów uruchamianych przy pomocy <a href="https://mochajs.org/" rel="noreferrer noopener">Mocha</a> ładne zliczanie pokrycia kodu. Od kuchni jednak Istanbula podzielić można na 3 podstawowe części:</p>
<ul>
<li><strong>instrumenter</strong> – przerabiającą kod źródłowy tak, by dało się wyliczyć pokrycie kodu;</li>
<li><strong>hook</strong> – podpinającą instrumenter do każdego wczytywanego w Node.js przez <code>require</code> modułu (analogicznie do tego, jak <a href="https://blog.comandeer.pl/html-w-node.html">opisywałem to kiedyś na blogu</a>);</li>
<li><strong>reporter</strong> – przerabiający wyniki uzyskane dzięki instrumenterowi na format zrozumiały dla maszyn i/lub ludzi.</li>
</ul>
<p>Najciekawszy jest zdecydowanie instrumenter, bo to w nim odbywa się cała magia związana z wyliczaniem pokrycia kodu. Zainstalujmy go zatem i zobaczmy, co robi z kodem źródłowym:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> istanbul-lib-instrument</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Teraz stwórzmy prosty skrypt, który będzie z niego korzystał:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">createInstrumenter</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'istanbul-lib-instrument'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumenter</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createInstrumenter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumentedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrumenter.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">instrumentSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`const hello = () =&gt; { // 3</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    console.log( 'Hello, world!' );</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">hello();`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'virtual-file.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( instrumentedCode ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy funkcję <code>createInstrumenter</code> (1) , która jest <a href="https://refactoring.guru/design-patterns/factory-method" rel="noreferrer noopener">fabryką</a> instrumenterów. Następnie tworzymy jeden z domyślnymi opcjami (2). Dzięki temu możemy wywołać jego metodę <code>instrumentSync</code> i przerobić podany kod JS (3). Warto przy tym zauważyć, że kod podajemy jako string. Wynika to z tego, że instrumenter jest w zdecydowanej większości przypadków wywoływany przy pomocy hooka, a hooki dostają kod wczytywanych modułów jako string. W naszym przypadku przykładowy kod do pokrycia to:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hello</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">hello</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Drugi parametr (4) to nazwa pliku, z jakiego pochodzi kod i najczęściej zostaje pobrana przez hook. W naszym wypadku nazwa pliku jest nieistotna, więc można tu wpisać cokolwiek. Na samym końcu wyświetlamy otrzymany kod (5).</p>
<p>Odpalmy zatem nasz skrypt:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">node</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> nazwa-pliku-ze-skryptem</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Powinniśmy uzyskać coś takiego:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(){</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"virtual-file.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> hash</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"c0f42c29fc01993cc28c18384ed0d61fa827b717"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> global</span><span style="color:#D73A49;--shiki-dark:#F97583">=new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Function</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"return this"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)();</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> gcv</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"__coverage__"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> coverageData</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{path:</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"virtual-file.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,statementMap:{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:{start:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">14</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},end:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">3</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}},</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:{start:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">4</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},end:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">35</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}},</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"2"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:{start:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">5</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},end:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">5</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">8</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}}},fnMap:{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:{name:</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"(anonymous_0)"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,decl:{start:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">14</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},end:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">15</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}},loc:{start:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">20</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},end:{line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">3</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,column:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}},line:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}},branchMap:{},s:{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"2"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},f:{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">},b:{},_coverageSchema:</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1a1c01bbd47fc00a2c39e90264f33305004495a9"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,hash:</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"c0f42c29fc01993cc28c18384ed0d61fa827b717"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> coverage</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">global[gcv]</span><span style="color:#D73A49;--shiki-dark:#F97583">||</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(global[gcv]</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{});</span><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverage[path]</span><span style="color:#D73A49;--shiki-dark:#F97583">||</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverage[path].hash</span><span style="color:#D73A49;--shiki-dark:#F97583">!==</span><span style="color:#24292E;--shiki-dark:#E1E4E8">hash){coverage[path]</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverageData;}</span><span style="color:#D73A49;--shiki-dark:#F97583">var</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> actualCoverage</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverage[path];{</span><span style="color:#6A737D;--shiki-dark:#6A737D">// @ts-ignore</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#D73A49;--shiki-dark:#F97583">=function</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(){</span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> actualCoverage;};}</span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> actualCoverage;}</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().s[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hello</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().f[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().s[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);};</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cov_2mvg7hpc0f</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().s[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">hello</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jak widać, kod został mocno przerobiony. Co się jednak tak właściwie dzieje? Otóż Istanbul dorzucił do każdego wyrażenia w naszym kodzie JS licznik, w postaci <code>mapaPokrycia.s[ numerWyrażenia ]</code>. Na początku <code>mapaPokrycia</code> zawiera listę wszystkich występujących w kodzie wyrażeń wraz z liczbą ich wywołań w trakcie testów (na start wynoszącą <code>0</code>). Jeśli po zakończeniu testów przy danym wyrażeniu w mapie wciąż będzie widnieć <code>0</code>, to znaczy, że nie zostało ono przetestowane.</p>
<p>Sprawdzanie natomiast, które wyrażenie zostało wykonane, odbywa się poprzez “doklejenie” do wyrażenia odpowiedniej inkrementacji licznika, np.:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">mapaPokrycia.s[ </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ]</span><span style="color:#D73A49;--shiki-dark:#F97583">++</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tym sposobem, gdy test dojdzie do linijki z <code>console.log</code>, równocześnie zmieni się wartośc licznika dla tego <code>console.log</code>. Dostawienie inkrementacji bezpośrednio przed sprawdzanym wyrażeniem pozwala także na sprawdzanie wyrażeń wewnątrz <code>if</code>–ów i innych bloków (np. funkcji).</p>
<p>Osobne liczniki istnieją dla funkcji czy odgałęzień <code>if</code>–ów. I połączenie tych wszystkich liczników daje nam pełny obraz pokrycia kodu w danym pliku. Znając liczbę wszystkich wyrażeń w danym pliku i wiedząc, które z nich nie zostały wywołane ani razu, obliczenie procentowego pokrycia kodu testami jest już proste.</p>
<p>Warto tutaj także dodać, w jaki sposób Istanbul po zakończeniu testów jest w stanie zebrać dane ze wszystkich plików. Wszystkie mapy pokrycia przechowywane są bowiem w… zmiennej globalnej (w przypadku Node.js to zmienna doczepiona do <code>global</code>). To tak naprawdę jedyny sposób, żeby zebrać w jednym miejscu dane z wielu modułów, które w innym wypadku są od siebie całkowicie odizolowane. Zatem jeśli szukacie dobrego wykorzystania zmiennych globalnych, to właśnie ono – zbieranie informacji o code coverage.</p>
<h2 id="implementacja"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#implementacja">Implementacja</a></h2>
<p>Skoro wiemy już, jak działa narzędzie do code coverage, zabierzmy się do napisania własnego! Oczywiście będzie ono o wiele prostsze od Istanbula i będzie zliczać wyłącznie pokrycie funkcji.</p>
<h3 id="przygotowanie-środowiska"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#przygotowanie-środowiska">Przygotowanie środowiska</a></h3>
<p>Na sam początek trzeba sobie przygotować środowisko pracy. Stwórzmy zatem katalog <code>coverage-sample</code> i wygenerujmy plik <code>package.json</code> przy pomocy komendy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> init</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> -y</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Z racji tego, że nasze rozwiązanie będzie działać podobnie do pokazanego wcześniej <code>nyc</code>, musimy dodać jeszcze pole <code>bin</code> do wygenerowanego <code>package.json</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"bin"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"index.js"</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Teraz pora na zainstalowanie zależności. Będą nam potrzebne trzy:</p>
<ul>
<li><a href="https://www.npmjs.com/package/@babel/core" rel="noreferrer noopener"><code>@babel/core</code></a> – zajmująca się dodawaniem liczników do kodu;</li>
<li><a href="https://www.npmjs.com/package/pirates" rel="noreferrer noopener"><code>pirates</code></a> – zajmująca się&nbsp;dodawaniem dodawania przez Babela;</li>
<li><a href="https://www.npmjs.com/package/@babel/types" rel="noreferrer noopener"><code>@babel/types</code></a> – zawierająca typy dla Babela, bo stwierdzili, że trzymanie ich osobno będzie wygodniejsze.</li>
</ul>
<p>Wypada je zatem zainstalować:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @babel/core</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> pirates</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @babel/types</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h3 id="główny-plik"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#główny-plik">Główny plik</a></h3>
<p>Mając już postawione środowisko, możemy przystąpić do pisania kodu. Zaczniemy od pliku <code>index.js</code>, który będzie równocześnie programem wykonywalnym (odpowiednikiem <code>nyc</code>):</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">#!/usr/bin/env node</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// ↑1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> addHook</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'./src/hook'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrument</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'./src/instrumenter'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> report</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'./src/reporter'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">global.__coverage__ </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {}; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">addHook</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( instrument ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> entryPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( process.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(), process.argv[ </span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( entryPath ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">report</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( global.__coverage__ ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 10</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na samym początku wstawiamy <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)" rel="noreferrer noopener">shebang</a> (1), który informuje terminal, jaki program powinien być użyty do uruchomienia tego pliku (w tym wypadku Node.js). Następnie załączamy poszczególne elementy naszego narzędzia (do ich tworzenia przystąpimy za chwilę): hook (2), instrumenter (3) oraz reporter (4). Tworzymy też&nbsp;globalny obiekt, który będzie przechowywał informacje o pokryciu kodu (5). Cała magia ukryta jest za dodaniem hooka (6). Funkcja ta jako argument przyjmuje instrumenter – bo chcemy, żeby wszystkie załączane pliki były instrumentowane. Następnie ustalamy ścieżkę do modułu z testami (7) przy użyciu funkcji <code>resolve</code> z wbudowanego modułu <code>path</code> (8). Ścieżkę tworzymy łącząc ścieżkę do katalogu roboczego (a więc katalogu, z którego ktoś odpalił nasze narzędzie) oraz ścieżkę przekazaną do samego programu. Tablica <code>process.argv</code> zawiera wszystkie argumenty z linii poleceń:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">coverage-sample</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /ścieżka</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym wypadku <code>/ścieżka</code> jest 3 elementem tej tablicy (po ścieżce Node’a oraz nazwie samego programu) – stąd w kodzie wykorzystujemy <code>process.argv[ 2 ]</code>.</p>
<p>Następnie po prostu wczytujemy moduł z testami (9), co powoduje wykonanie się zawartego w nim kodu, a przy okazji – liczników wstrzykniętych przez nasze narzędzie. Na samym końcu wyświetlamy wyniki zebrane w zmiennej globalnie w przyjaznej użytkownikowi formie (10).</p>
<h3 id="reporter"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#reporter">Reporter</a></h3>
<p>Przejdźmy zatem do pliku reportera – <code>src/reporter.js</code> – bo jest najmniej ciekawy i w sumie istnieje tylko po to, żeby całość <em>spełniała normę estetyczną</em>. Plik wygląda tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">coverageData</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">all</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">covered</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Object.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">entries</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( coverageData ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">reduce</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">data</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, [ , { </span><span style="color:#E36209;--shiki-dark:#FFAB70">functions</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ] ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		data.all </span><span style="color:#D73A49;--shiki-dark:#F97583">+=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> functions.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		data.covered </span><span style="color:#D73A49;--shiki-dark:#F97583">+=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> functions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">reduce</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">covered</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">func</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> covered </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> func;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> data;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}, { all: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, covered: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`All functions: ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> all</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">Covered functions: ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> covered</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">Coverage: ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Math</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">round</span><span style="color:#032F62;--shiki-dark:#9ECBFF">( </span><span style="color:#24292E;--shiki-dark:#E1E4E8">covered</span><span style="color:#D73A49;--shiki-dark:#F97583"> /</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> all</span><span style="color:#D73A49;--shiki-dark:#F97583"> *</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ) </span><span style="color:#032F62;--shiki-dark:#9ECBFF">}%`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Cały plik składa się z eksportowanej funkcji (1), która jako argument przyjmuje zmienną globalną z danymi pokrycia. Wyciągamy z niej potrzebne nam dane przy pomocy <code>Object.entries</code> + <code>[].reduce</code> (2), formatujemy i wyświetlamy w konsoli (3). W sumie tyle – nic ciekawego się&nbsp;tutaj nie dzieje.</p>
<h3 id="hook"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#hook">Hook</a></h3>
<p>Przejdźmy zatem do hooka (w pliku <code>src/hook.js</code>), bo ten jest już&nbsp;zdecydowanie bardziej ciekawy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">addHook</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'pirates'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	addHook</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/tests/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> code; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumentedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code, path ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrumentedCode; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}, { exts: [ </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] } ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Cały plik to znowu eksportowana funkcja (1), która jako argument tym razem przyjmuje instrumenter. W jej środku wywołujemy funkcję <code>addHook</code> (2) z biblioteki <code>pirates</code> (3). Nasz hook będzie działał&nbsp;tylko na pliki z rozszerzeniem <code>.js</code> (4). Na samym początku odsiewamy wszystkie pliki testów (5). Dodanie liczników do plików testów nie ma sensu. Nie dość, że nie interesuje nas pokrycie kodu w testach a jedynie w samym kodzie aplikacji, to dodatkowo może to zaciemniać obraz. Stąd kod testów zostawiamy w spokoju i zwracamy go niezmieniony (6). Natomiast całą&nbsp;resztę kodu traktujemy naszym instrumenterem (7) i tak zmodyfikowany kod zwracamy (8). Dzięki temu każde wczytanie dowolnego modułu wczyta kod z dodanymi licznikami.</p>
<h3 id="instrumenter"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#instrumenter">Instrumenter</a></h3>
<p>W końcu przyszła pora na danie główne – instrumenter! Zacznijmy od stworzenia pliku <code>src/instrumenter.js</code>.  Jego główną częścią&nbsp;będzie funkcja <code>instrument</code>, którą będziemy eksportować, a która przyjmuje dwa parametry – kod do przetworzenia i ścieżkę do pliku, z którego kod pochodzi:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">filePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrument;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wewnątrz tej funkcji będziemy chcieli pobrać wszystkie deklaracje funkcji i wstawić do nich licznik. W przeciwieństwie do <a href="https://blog.comandeer.pl/bujajac-sie-na-galezi-ast.html">mojego wcześniejszego artykułu o AST</a>, tym razem posłużymy się API wyższego poziomu, jakie jest udostępniane przez pakiet <code>@babel/core</code> – funkcją <code>transformSync</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">transformSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'@babel/core'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">filePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumentedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> transformSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		plugins: [ </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> codeCoverage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					visitor: { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						FunctionDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrumentedCode.code; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na samym początku importujemy tę funkcję (1). Następnie, wewnątrz <code>instrument</code>, wywołujemy ją, przekazując jej jako 1. parametr kod do przetworzenia (2). Drugi parametr to obiekt opcji. W naszym wypadku chcemy tam przekazać plugin, który będzie dodawał liczniki do kodu. W tym celu musimy dorzucić opcję <code>plugins</code> (3) i sam plugin w formie funkcji (4). Babel pozwala pluginom używać <a href="https://en.wikipedia.org/wiki/Visitor_pattern" rel="noreferrer noopener">wzorcu Wizytatora</a>. Innymi słowy: można stworzyć funkcję, która będzie “wizytować” każdy węzeł w AST.  Co więcej, można wybrać typ węzłów dla naszego wizytatora, dzięki czemu odwiedzi tylko odpowiednie węzły. Żeby stworzyć wizytatora, nasz plugin musi zwróci obiekt (5) z własnością <code>visitor</code> (6). Natomiast sam wizytator musi mieć nazwę taką samą, jak typ węzła, który chcemy “wizytować” – w naszym wypadku <code>FunctionDeclaration</code> (7). Od tej chwili nasz wizytator będzie wywoływany za każdym razem, gdy Babel natrafi na deklarację funkcji. Na samym końcu zwracamy przetworzony kod (8).</p>
<p>Funkcja <code>transformSync</code> zawiera w sobie wszystko to, co wcześniej robiliśmy przy pomocy odpowiednich pakietów Babela, a więc: parsuje kod do AST, trawersuje go i modyfikuje, a na końcu na powrót generuje kod JS. To w połączeniu ze wzorcem Wizytatora tworzy przyjemny sposób na modyfikowanie AST.</p>
<p>Co jednak dokładnie będzie robił nasz wizytator? Wiemy, że ma dodawać liczniki do funkcji – tak, aby zliczać, czy są wywoływane. Najpewniej będzie to zrobić, dodając go jako pierwsze wyrażenie w ciele funkcji:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> deklaracjaFunkcji</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // Tutaj chcemy umieścić licznik.</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    […]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Umieszczenie licznika w ciele funkcji gwarantuje nam, że zostanie uruchomiony jedynie wówczas, gdy sama funkcja zostanie wywołana. Natomiast umieszczenie go na samym początku ciała funkcji pozwala ominąć nam wszystkie klauzule strażnicze i inne potencjalne problemy, które mogłyby zaciemnić obraz. Co więcej, sam początek ciała nie nastręcza trudności związanych choćby z potrzebą wstawiania licznika przed <code>return</code>, jeśli chcielibyśmy go umieszczać na końcu ciała funkcji (bo znajdując się po <code>return</code> nigdy nie zostałby uruchomiony).</p>
<p>Zatem kod naszego wizytatora powinien wyglądać tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">FunctionDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( path ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> counter</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createCounter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( filePath ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">unshiftContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, counter ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na samym początku tworzymy licznik (1), a następnie wstawiamy go na początek ciała funkcji (2).</p>
<p>Zanim przejdziemy do funkcji tworzącej licznik, wypada też zadbać o to, by nasz instrumenter zapisywał informacje o funkcjach i ich pokryciu dla każdego pliku, jaki przetwarza. W tym celu stwórzmy tablicę <code>functions</code>, która później będzie lądować w zmiennej globalnej:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">filePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> functions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumentedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> transformSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		plugins: [</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> codeCoverage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					visitor: {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						FunctionDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> counter</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createCounter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( filePath, functions.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">							path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">unshiftContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, counter );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">							functions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	global.__coverage__[ filePath ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		functions</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrumentedCode.code;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na samym początku funkcji instrumentującej tworzymy pustą tablicę <code>functions</code> (1). Za każdym razem, gdy wizytator natrafia na kolejną funkcję, wrzucamy do tej tablicy liczbę wywołań dla kolejnej funkcji (2). Natomiast sama funkcja tworząca liczniki powinna też wiedzieć, dla której konkretnie funkcji go tworzy – a więc dostawać indeks tej funkcji w tablicy <code>functions</code>. W tym celu zastosowane zostało <code>functions.length</code> (3). Działa to, ponieważ długość tablicy jest zawsze o jeden większa niż numer ostatniego indeksu. A to oznacza, że nowo dodawana funkcja ma indeks równy długości tablicy. Na samym końcu dorzucamy tablicę <code>functions</code> do globalnej zmiennej <code>__coverage__</code> pod kluczem stworzonym ze ścieżki do pliku modułu (4).</p>
<p>Przejdźmy zatem do funkcji tworzącej sam licznik:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	expressionStatement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	stringLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	numericLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	updateExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'@babel/types'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createCounter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">fileName</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">index</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> expressionStatement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		updateExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'++'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">					memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">							identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'global'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">							identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'__coverage__'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">							false</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						stringLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( fileName ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">						true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">					identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'functions'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">					false</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				numericLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( index ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">				true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wygląda ona przerażająco, ale głównie z powodu sporej liczby zagnieżdżeń.</p>
<div class="note" role="note" aria-labelledby="note-42"><p class="note__label" id="note-42">Dygresja</p><div class="note__content"><p>W przypadku Reacta istnieje bardzo podobny problem: węzły potomne w <a href="https://reactjs.org/docs/faq-internals.html" rel="noreferrer noopener">vDOM</a> tworzy się przy pomocy zagnieżdżenia w węźle-rodzicu. Tam jednak problem ten został rozwiązany przy pomocy <a href="https://reactjs.org/docs/introducing-jsx.html" rel="noreferrer noopener">JSX</a>. Niemniej można łatwo sprawdzić, że <a href="https://babeljs.io/repl#?browsers=&amp;build=&amp;builtIns=false&amp;spec=false&amp;loose=false&amp;code_lz=GYVwdgxgLglg9mABAQQA6oBQEpEG8BQiiATgKZQjFIA8AJjAG4B8hRi1AFgIxPUDOqAIZhefKMQQBzXqQC2TAHKCAVnwCE1APRzemsRLDStA4bu4siW-swDc-AL758aTFhtA&amp;debug=false&amp;forceAllTransforms=false&amp;shippedProposals=false&amp;circleciRepo=&amp;evaluate=false&amp;fileSize=false&amp;timeTravel=false&amp;sourceType=module&amp;lineWrap=true&amp;presets=es2015%2Creact%2Cstage-2&amp;prettier=false&amp;targets=&amp;version=7.12.12&amp;externalPlugins=" rel="noreferrer noopener">JSX faktycznie jest transpilowany do zagnieżdżeń</a>. Niestety, z tego, co mi wiadomo, nikt jeszcze nie przystosował JSX na potrzeby generowania AST.</p></div></div>
<p>Tak naprawdę jedyne, co ten kod robi, to tworzy inkrementację zmiennej globalnej w postaci <code>global.__coverage__[ 'ścieżka/do/pliku' ].functions[ 0 ]++</code>. Sama inkrementacja to <code>updateExpression</code> (1). Natomiast odwołania do poszczególnych własności to kolejne <code>memberExpression</code>. Warto zwrócić tutaj uwagę na trzeci parametr tej funkcji (2). Określa on, czy dana własność ma być zapisana z kropką, czy z nawiasem. Dla ścieżki do pliku oraz liczby przyjmuje on wartość <code>true</code>, wymuszając nawias. Gdybyśmy zastosowali w tych wypadkach <code>false</code>, prowadziłoby to do błędu składniowego (<code>global.__coverage__.'ścieżka/do/pliku'.functions.0++</code>), stąd Babel dla takiego przypadku rzuca błędem. Wszystkie funkcje generujące nowe węzły pochodzą z pakietu <code>@babel/types</code> (3).</p>
<p>Cały kod instrumentera wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	expressionStatement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	stringLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	numericLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	updateExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'@babel/types'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">transformSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'@babel/core'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> instrument</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">filePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> functions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> instrumentedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> transformSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( code, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		plugins: [</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> codeCoverage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					visitor: {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						FunctionDeclaration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">							const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> counter</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createCounter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( filePath, functions.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">							path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">unshiftContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, counter );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">							functions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	global.__coverage__[ filePath ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		functions</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrumentedCode.code;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createCounter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">fileName</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">index</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> expressionStatement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		updateExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'++'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">			memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">					memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						memberExpression</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">							identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'global'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">							identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'__coverage__'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">							false</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">						),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">						stringLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( fileName ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">						true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">					),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">					identifier</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'functions'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">					false</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">				),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">				numericLiteral</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( index ),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">				true</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> instrument;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h3 id="testowanie-narzędzia"><a class="header-anchor" href="https://blog.comandeer.pl/jak-dziala-narzedzie-do-code-coverage#testowanie-narzędzia">Testowanie narzędzia</a></h3>
<p>W celu przetestowania narzędzia najlepiej będzie stworzyć przykładowy projekt. Stwórzmy zatem wewnątrz katalogu <code>coverage-sample</code> dodatkowy katalog – <code>sample-project</code> – i otwórzmy go w terminalu, by wygenerować dla niego plik <code>package.json</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> init</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> -y</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Do tak wygenerowanego pliku musimy dodać dwie rzeczy: nasz pakiet <code>coverage-sample</code> w formie zależności oraz wykorzystujący nasz pakiet skrypt testujący. Cały plik <code>package.json</code> powinien po zmianach wyglądać mniej więcej tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "name"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"sample-project"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "version"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1.0.0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "description"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "main"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"src/index.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "directories"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "test"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"tests"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "scripts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "test"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"coverage-sample ./tests/"</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "devDependencies"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "coverage-sample"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">".."</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "keywords"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [],</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "author"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "license"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ISC"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Nasze narzędzie znalazło się w sekcji <code>devDependencies</code> (1). Zamiast wersji użyjemy <code>..</code>, żeby wskazać miejsce na dysku, w którym znajduje się nasz pakiet (2). W tym wypadku znajduje się on katalog wyżej. Natomiast w sekcji <code>scripts</code> (3) znalazł się skrypt <code>test</code>, który wykorzystuje nasz pakiet jako program wykonywalny (4). Jest to możliwe, ponieważ w <code>coverage-sample</code> dodaliśmy pole <code>bin</code> do <code>package.json</code>.</p>
<p>Teraz wystarczy “zainstalować” nasz pakiet:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dzięki tej komendzie npm utworzy odpowiednie linki symboliczne do naszego <code>coverage-sample</code> i będzie można go bez przeszkód wykorzystywać w przykładowym projekcie.</p>
<p>Natomiast sam projekt składać się będzie z dwóch plików: <code>src/index.js</code>, zawierającego kod aplikacji, oraz <code>tests/index.js</code>, zawierającego testy.</p>
<p>Plik <code>src/index.js</code> prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> notMain</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Whatever'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	main,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	notMain</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Z kolei plik <code>tests/index.js</code> wygląda tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">main</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'../src'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jak widać, “testy” wykonują tylko jedną funkcję – <code>main</code>. Ten brak pokrycia powinien zostać wykazany przez nasze narzędzie do code coverage. Sprawdźmy, czy faktycznie tak się stanie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> test</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli wszystko poszło zgodnie z planem, w terminalu powinna się pokazać następująca informacja:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>All functions: 2</span></span>
<span class="line"><span>Covered functions: 1</span></span>
<span class="line"><span>Coverage: 50%</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><hr>
<p>I to by było na tyle! Mam nadzieję, że artykuł choć trochę przybliżył działanie narzędzi pokroju IstanbulJS. Oczywiście <a href="https://github.com/Comandeer/coverage-sample" rel="noreferrer noopener">cały kod źródłowy dostępny jest na GitHubie</a>. Miłej zabawy!</p>
<p>PS wygląda, że zapomniałem o ważnej rocznicy: 9 stycznia 2021 mój <a href="https://tutorials.comandeer.pl/html5-blog.html" rel="noreferrer noopener">tutorial o semantycznym blogu w HTML</a> obchodził swoje 10 urodziny! Nigdy nie sądziłem, że będzie żył tak długo, a już&nbsp;tym bardziej, że będę dbał o to, by był jak najbardziej aktualny.</p>
]]></content>
		</entry>
	
</feed>
