<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Zápisky | BHPS Labs</title>
    <link>https://bhpslabs.com/cs/blog</link>
    <description>Texty lidí z BHPS Labs o vývoji, bezpečnosti a provozu softwaru: nástroje, chyby a postupy, které zrovna řešíme v práci na vlastních produktech.</description>
    <language>cs</language>
    <atom:link href="https://bhpslabs.com/cs/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Thu, 15 Oct 2026 00:00:00 GMT</lastBuildDate>
    <image>
      <url>https://bhpslabs.com/logo.png</url>
      <title>Zápisky | BHPS Labs</title>
      <link>https://bhpslabs.com/cs/blog</link>
    </image>
    <item>
      <title>TanStack Start 1.0 je venku: proč na něm stavíme a proč jsem opustil Next.js</title>
      <link>https://bhpslabs.com/cs/blog/tanstack-start-1-0</link>
      <guid isPermaLink="true">https://bhpslabs.com/cs/blog/tanstack-start-1-0</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Jan Szotkowski</dc:creator>
      <category>tanstack</category>
      <category>tanstack-start</category>
      <category>react</category>
      <category>nextjs</category>
      <category>seo</category>
      <category>geo</category>
      <category>developer-experience</category>
      <description>TanStack Start má po letech vývoje stabilní verzi 1.0. Co přesně vyšlo, co o tom píše Tanner Linsley a proč na Startu už běží naše dva produkční projekty, bhpslabs.com a udalist.app. Řeč bude o developer experience, o učící křivce a o tom, jak se na Startu staví SEO a GEO.</description>
      <content:encoded><![CDATA[<p><strong>TanStack Start 1.0 je oficiálně stabilní.</strong> Tanner Linsley to ohlásil 13. října 2026 na <a href="https://tanstack.com/blog/tanstack-start-1-0">blogu TanStacku</a> a já na to čekal dlouho. Ne proto, že bych do té doby Start nepoužíval. Používal. Čekal jsem, až budu moct klientům, kolegům a každému, kdo se zeptá, říct: <em>je to hotové, je to stabilní, klidně na tom stavte.</em></p>
<p>Ptají se mě na to často. „Jakou frontendovou technologii bys dnes zvolil na nový projekt?" Moje odpověď je už nějakou dobu stejná: <strong>TanStack Start, a k němu zbytek TanStacku.</strong> Je to první věc, po které sáhnu, když zakládám nový projekt. A momentálně nemám jediný důvod to měnit.</p>
<p>V tomhle článku shrnu, co verze 1.0 znamená, co k tomu píše sám Tanner, a hlavně ukážu, jak Start používáme v produkci na dvou projektech, na tomhle webu <a href="https://bhpslabs.com">bhpslabs.com</a> a na aplikaci <a href="https://udalist.app">udalist.app</a>. Bez teorie, na skutečném kódu.</p>
<h2 id="co-znamena-1-0">Co znamená „1.0"<a href="#co-znamena-1-0" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Start běží v produkci u spousty firem už dlouho. Od malých projektů po enterprise týmy, a jak Tanner sám přiznává, řada z nich ho nasadila ještě v betě, i když jim to tým rozmlouval. My jsme na tom byli stejně.</p>
<p>Verze 1.0 proto není o tom, že by Start najednou začal fungovat. Je to <strong>formální závazek k API.</strong> Můžete si Start vybrat, naučit se ho, postavit na něm produkt a nemusíte se bát, že vám za půl roku překope celý způsob, jakým aplikace funguje. Pro firmu, která na technologii sází peníze a lidi, je tohle ta nejdůležitější vlastnost ze všech.</p>
<p>Tanner taky otevřeně píše, že cesta od release candidate k 1.0 trvala déle, než čekal. Mezitím do frameworku přibyla práce na kompilátoru, streamingu, hydrataci a hranicích mezi serverem a klientem. Manuel Schiller a Florian Pellet se zaměřili na výkon SSR a v jejich benchmarku dostali zhruba <strong>5,5× vyšší propustnost</strong>. Christopher Horobin stojí za tím, že extrémně silné typy routeru nezaseknou editor. To jsou věci, které v changelogu nevypadají dramaticky, ale v produkci je poznáte.</p>
<h2 id="co-tanner-v-clanku-rika-a-proc-s-nim-souhlasim">Co Tanner v článku říká (a proč s ním souhlasím)<a href="#co-tanner-v-clanku-rika-a-proc-s-nim-souhlasim" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Celý článek doporučuju přečíst v originále. Pro ty, kdo nemají čas, tady jsou body, které mi mluví z duše.</p>
<h3 id="vsechno-zacalo-routerem">Všechno začalo routerem<a href="#vsechno-zacalo-routerem" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Start nevznikl jako framework. Vznikl jako router, u kterého Tanner chtěl, aby <strong>věděl všechno</strong>: jaké cesty existují, jaké parametry potřebují, jaké query parametry jsou platné, co vrací loader a jaký kontext je kde dostupný. A aby vám editor při každé změně řekl, co dalšího se musí upravit.</p>
<p>Před pár lety tomu spousta lidí nevěřila. Dnes je to podle mě největší důvod, proč TanStack Router a Start používat.</p>
<h3 id="jina-cesta-nez-vsechno-na-serveru">Jiná cesta než „všechno na serveru"<a href="#jina-cesta-nez-vsechno-na-serveru" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Tanner popisuje, že React ekosystém se v posledních letech hodně zúžil na jednu představu o tom, jak má aplikace vypadat. Next.js vsadil na React Server Components jako základ frameworku i jako ideologii. Start jde jinudy: <strong>aplikace zůstane aplikací</strong>, se stavem, klientskou navigací a cachováním dat. Server rendering, streaming a server funkce přidáte tam, kde dávají smysl, ale nemusíte kvůli nim přepisovat celé uvažování o aplikaci.</p>
<p>Každá route si může vybrat, jestli se vykreslí na serveru, jestli na serveru jen načte data, nebo jestli poběží čistě na klientu. Server funkce jsou typované a volají se z loaderu i z event handleru. Middleware umí běžet na klientu i na serveru a typy kontextu jdou skrz.</p>
<h3 id="rsc-jako-volba-ne-povinnost">RSC jako volba, ne povinnost<a href="#rsc-jako-volba-ne-povinnost" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Zajímavá pasáž je o React Server Components. Tým na nich strávil hodně času a Start je podporuje, ale tak, aby nepřevzaly kontrolu nad celou aplikací. A pointa: na tanstack.com je nakonec <strong>vypnuli</strong>, protože bylo čím dál těžší uvažovat o tom, co se kam posílá, a to platilo pro lidi i pro AI modely. Vypnutí přitom neznamenalo přepisovat aplikaci. To je přesně ten druh svobody, který od frameworku chci.</p>
<h3 id="hosting-kdekoli">Hosting kdekoli<a href="#hosting-kdekoli" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Start stojí na Vite a Nitru (a nově umí i Rsbuild), takže nasazení není svázané s jedním poskytovatelem. Netlify, Vercel, Cloudflare, Railway, Render, nebo vlastní Docker kontejner s Node serverem, jak to děláme my.</p>
<h3 id="typy-pomahaji-i-ai-agentum">Typy pomáhají i AI agentům<a href="#typy-pomahaji-i-ai-agentum" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Tohle považuju za nejvíc podceňovaný argument. Striktní typy a explicitní API vznikly pro lidi, ale <strong>AI agentům pomáhají úplně stejně</strong>. Když agent změní parametr route nebo vstup server funkce, TypeScript mu hned ukáže všechna místa, která s tou změnou nesouhlasí. TanStack navíc začal přidávat do npm balíčků takzvané skills (přes TanStack Intent), takže agent nemusí spoléhat na zastaralá trénovací data, ale čte aktuální doporučené postupy přímo z balíčku.</p>
<h2 id="proc-jsem-opustil-next-js">Proč jsem opustil Next.js<a href="#proc-jsem-opustil-next-js" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Next.js jsem používal dlouho a nechci ho shazovat. Umí skvěle vysvětlit a prodat svou myšlenku a spoustu lidí to naučil. Jenže čím dál víc jsem měl pocit, že <strong>bojuju s frameworkem místo toho, abych stavěl produkt</strong>.</p>
<ul>
<li><code>"use client"</code> a <code>"use server"</code> direktivy rozhodují o tom, kde kód běží, a hranice nejsou vždycky vidět.</li>
<li>Cachování se v několika verzích za sebou změnilo od základu a bylo těžké odhadnout, co je cachované, kde a jak dlouho.</li>
<li>Mentální model App Routeru je pro juniora (a upřímně i pro seniora) těžký: serverové komponenty, klientské komponenty, co smí importovat co.</li>
<li>Typová bezpečnost routování je spíš doplněk než základ. Query parametry jsou v podstatě <code>string | string[] | undefined</code>.</li>
</ul>
<p>U Startu je to opačně. <strong>Komponenty jsou komponenty, routy jsou routy, a když potřebuju server, napíšu to explicitně.</strong> Nemusím mít v hlavě druhý model aplikace, abych prošel běžným dnem.</p>
<p>S Tannerem jsem se potkal i osobně v Amsterdamu a ten rozhovor mi potvrdil, co jsem z kódu cítil dávno: za TanStackem je člověk, kterému jde o to, aby se nástroje dobře používaly, ne o to, aby se dobře prodávaly.</p>
<h2 id="developer-experience-a-ucici-krivka">Developer experience a učící křivka<a href="#developer-experience-a-ucici-krivka" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>DX je u Startu podle mě o třídu dál a nejlépe to ukáže pár konkrétních věcí.</p>
<p><strong>Typované routy bez generování v hlavě.</strong> Router si sám generuje strom rout (<code>routeTree.gen.ts</code>). Odkaz na neexistující stránku nebo chybějící parametr je chyba při kompilaci, ne 404 v produkci. Na udalist.app máme přes 80 <code>&#x3C;Link></code> komponent a každá z nich je typově zkontrolovaná.</p>
<p><strong>Query parametry jako typovaný stav.</strong> <code>validateSearch</code> (na Udalistu ho používá sedm rout) udělá z query parametrů validovaný a typovaný objekt. Filtry, stránkování nebo stav formuláře v URL přestanou být zdrojem chyb.</p>
<p><strong>Explicitní server.</strong> Server funkce je obyčejná funkce, kterou zavoláte. Vidíte, co jde dovnitř, co jde ven a že běží na serveru. Žádná magie.</p>
<p><strong>Učící křivka.</strong> Kdo zná React a umí trochu TypeScript, je ve Startu produktivní za pár dní. Nemusí se učit nový paradigma, jen router. A kdo zná TanStack Query, je doma ještě rychleji. U Next.js App Routeru jsem viděl zkušené vývojáře tápat týdny.</p>
<h2 id="seo-a-geo-tady-start-opravdu-vynika">SEO a GEO: tady Start opravdu vyniká<a href="#seo-a-geo-tady-start-opravdu-vynika" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>O SEO a GEO (Generative Engine Optimization, tedy optimalizaci pro AI asistenty typu ChatGPT, Claude nebo Perplexity) jsme na tomhle webu strávili hodně času. A Start nám v tom nestál v cestě ani jednou. Naopak.</p>
<h3 id="metadata-u-kazde-route">Metadata u každé route<a href="#metadata-u-kazde-route" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Každá route má funkci <code>head()</code>, která vrací titulek, popis, canonical, hreflang, Open Graph i JSON-LD. Je typovaná, zná parametry route a běží při SSR, takže crawler dostane hotové HTML. Takhle zjednodušeně vypadá route tohoto článku:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">export</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> Route</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> createFileRoute</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'/$locale/blog/$slug'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">)({</span></span>
<span class="line"><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">    beforeLoad</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: ({ </span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">params</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> }) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">!</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">findPost</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(posts, params.locale, params.slug)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">throw</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> notFound</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">();</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    },</span></span>
<span class="line"><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">    head</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: ({ </span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">params</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> }) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> post</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> findPost</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(posts, params.locale, params.slug);</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        return</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> pageHead</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            title: post.title,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            description: post.description,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            ogType: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'article'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            alternates, </span><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">// hreflang na překlad článku</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            nodes: [</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">blogPostingNode</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({ headline: post.title </span><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">/* … */</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> })], </span><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">// JSON-LD</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        });</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    },</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    component: ArticlePage,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">});</span></span>
<span class="line"></span></code></pre>
<p>Všimněte si i <code>beforeLoad</code>. Starý tvar adresy přesměruje trvalým 301 a neexistující článek vrátí skutečnou 404, ne prázdnou stránku se stavovým kódem 200. Na tom SEO hodně záleží.</p>
<h3 id="prerender-a-sitemap-primo-v-konfiguraci">Prerender a sitemap přímo v konfiguraci<a href="#prerender-a-sitemap-primo-v-konfiguraci" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Statické stránky necháváme předgenerovat při buildu a sitemap vzniká automaticky. Na obou projektech je to pár řádků ve <code>vite.config.ts</code>:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">tanstackStart</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    pages: </span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">getTanstackStartPages</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(),</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    prerender: { enabled: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, crawlLinks: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">false</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, autoStaticPathsDiscovery: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">false</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> },</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    sitemap: { enabled: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, host: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'https://bhpslabs.com/'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> },</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}),</span></span>
<span class="line"></span></code></pre>
<p>Na Udalistu má prerender navíc filtr, který vynechá přihlášenou část aplikace (dashboard, autentizaci). Marketingové stránky jsou tak statické a bleskové, aplikace za přihlášením běží jako klasická SPA.</p>
<h3 id="web-kteremu-rozumi-i-ai">Web, kterému rozumí i AI<a href="#web-kteremu-rozumi-i-ai" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Tady se ukazuje síla toho, že Start nic neschovává. Vlastní serverový vstup (<code>src/server.ts</code>) je obyčejný <code>fetch</code> handler, do kterého si můžete přidat cokoli. Na bhpslabs.com díky tomu:</p>
<ul>
<li><strong>vracíme stejnou stránku jako Markdown</strong>, když si o to AI agent řekne hlavičkou <code>Accept: text/markdown</code>. Agent nemusí parsovat HTML a dostane čistý text,</li>
<li><strong>skládáme Content-Security-Policy pro každou odpověď zvlášť</strong> z otisků inline skriptů, bez <code>unsafe-inline</code>,</li>
<li><strong>servírujeme soubory pod <code>/.well-known/</code></strong> pro objevování agenty (katalog, agent skills),</li>
<li>k tomu <code>robots.txt</code> s pravidly pro AI crawlery a <code>llms.txt</code>, který modelům říká, co děláme a kdy nás doporučit.</li>
</ul>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">export</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> default</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> {</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    async</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> fetch</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">(request</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> Request) </span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> wellKnown</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> await</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> wellKnownResponse</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(request);</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (wellKnown) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">return</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> wellKnown;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> markdown</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> wantsMarkdown</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(request.headers.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">get</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'accept'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">));</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> response</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> await</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> paraglideMiddleware</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(request, () </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> handler.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">fetch</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(request));</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (markdown) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">return</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> toMarkdownResponse</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(response);</span></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">        // … CSP z otisků inline skriptů, Vary, gzip</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        return</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> response;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    }</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">,</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">}</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"></span></code></pre>
<p>Udalist má velmi podobný vstup: Paraglide middleware, bezpečnostní hlavičky, Markdown pro úvodní stránku a <code>/.well-known/</code> soubory. Jednou vymyšlené, dvakrát použité.</p>
<h2 id="nase-projekty-v-produkci">Naše projekty v produkci<a href="#nase-projekty-v-produkci" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<h3 id="bhpslabs-com">bhpslabs.com<a href="#bhpslabs-com" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Tenhle web běží na TanStack Startu, TanStack Routeru, Reactu 19, Vite a Nitru, nasazený jako Node server v Dockeru. Je dvojjazyčný (čeština a angličtina) přes Paraglide a cesty mají tvar <code>/$locale/...</code>.</p>
<p>Pár věcí, které stojí za zmínku:</p>
<ul>
<li><strong>Blog</strong> (ano, i tenhle článek) se načítá z Markdown souborů přes vlastní Vite plugin, který je při buildu zvaliduje a převede do typovaných dat. Překlady článků spojuje složka, ne adresa.</li>
<li><strong>Poptávkový formulář</strong> odesílá data přes typovanou server funkci s validací a rate limitem:</li>
</ul>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">export</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> sendAuditRequest</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> createServerFn</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({ method: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'POST'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> })</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    .</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">validator</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">((</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">value</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> unknown</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">)</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> SendInput</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">!</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">isInput</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(value)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">throw</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> new</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> Error</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'Invalid input'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">);</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        return</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> value;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    })</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    .</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">handler</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">async</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ({ </span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">data</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> })</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> Promise</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">&#x3C;</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">SendResult</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">> </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">!</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">allow</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">getRequestIP</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({ xForwardedFor: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> }) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">??</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'unknown'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">return</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> { status: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'unavailable'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> };</span></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">        // … validace a odeslání e-mailu</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    });</span></span>
<span class="line"></span></code></pre>
<ul>
<li><strong>Statické HTML, cache a komprese</strong> řešíme přes Nitro <code>routeRules</code>: písma a assety s dlouhou cache, HTML s revalidací, předkomprimované JS a CSS.</li>
</ul>
<p>A výsledek? <strong>Lighthouse dává bhpslabs.com 100 / 100 / 100 / 100</strong> ve všech čtyřech kategoriích (Performance, Accessibility, Best Practices, SEO) a v nové kategorii <strong>Agentic Browsing 3 / 3</strong>, která měří, jak dobře se na webu vyzná AI agent. To není zásluha jednoho triku. Je to součet věcí výše: prerender, SSR bez zbytečného JavaScriptu, metadata u každé route a server, do kterého si můžete přidat, co potřebujete. Start nám v ničem z toho nepřekážel.</p>
<h3 id="udalist-app">udalist.app<a href="#udalist-app" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p><a href="https://udalist.app">Udalist</a> sbírá fotky a videa z akcí (svatby, oslavy, firemní eventy) do jedné sdílené galerie. Hosté naskenují QR kód a nahrávají přímo z prohlížeče v telefonu, bez aplikace a bez účtu. Je to naše vlajková loď a je tu víc aplikace než webu, takže je na něm vidět i druhá strana Startu:</p>
<ul>
<li><strong>33 souborů rout</strong> s layouty bez cesty: <code>_public</code> pro marketing, <code>_guest</code> pro hosty a <code>_secure</code> pro přihlášené. Každá skupina má svůj layout a svá pravidla, a adresa přitom zůstává čistá.</li>
<li><strong>TanStack Query</strong> žije v kontextu routeru, takže ho má k dispozici každý loader i <code>beforeLoad</code>. Data tahá z backendu v Kotlinu a v aplikaci ho používá kolem 25 souborů.</li>
<li><strong>TanStack Form</strong> obsluhuje devět formulářů, od kontaktu po nastavení zabezpečení akce.</li>
<li><strong>Sedm jazyků</strong> přes Paraglide, <strong>Sentry</strong> přes jeho oficiální SDK pro TanStack Start.</li>
<li><strong>A/B test úvodní stránky</strong> běží přes server funkci, která přečte cookie a nastaví hlavičky odpovědi.</li>
<li><strong>Cache pravidla pro Nitro</strong> jsou typovaná proti seznamu rout, který vygeneruje router. Když route přejmenujete nebo smažete, konfigurace cache přestane procházet kompilací:</li>
</ul>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">import</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> type</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> { FileRouteTypes } </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">from</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> '../src/routeTree.gen'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">type</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> AppFullPath</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> FileRouteTypes</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">[</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'fullPaths'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> MARKETING_PATHS_WITH_LONG_CACHE</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> [</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">    '/'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">    '/pricing'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">    '/faq'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">    '/blog/'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">    // …</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">as</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> const</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> satisfies</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> readonly</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> AppFullPath</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">[];</span></span>
<span class="line"></span></code></pre>
<p>Překlep v adrese tak není tichá chyba v produkci, kdy stránka najednou přijde bez cache a bez bezpečnostních hlaviček. Je to červená čára v editoru. Přesně tohle mám na TanStacku rád.</p>
<p>Oba projekty mají schválně stejný základ. Jsme čtyřčlenný tým a nechceme udržovat dvě různé sestavy. Co vyřešíme na jednom, přeneseme na druhý.</p>
<h2 id="ekosystem-je-ta-nejvetsi-vyhoda">Ekosystém je ta největší výhoda<a href="#ekosystem-je-ta-nejvetsi-vyhoda" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Start sám o sobě je skvělý, ale jeho skutečná síla je v tom, že není sám:</p>
<ul>
<li><strong>TanStack Query</strong> pro serverová data a cache, standard, který znáte,</li>
<li><strong>TanStack Router</strong> s typovou bezpečností, jakou jinde nenajdete,</li>
<li><strong>TanStack Form</strong> pro typované formuláře bez ceremonií,</li>
<li><strong>TanStack Table</strong> a <strong>Virtual</strong> pro tabulky a dlouhé seznamy,</li>
<li>a další: Store, Pacer, DB, AI, Devtools, Markdown…</li>
</ul>
<p>Všechny knihovny mají stejnou filozofii: headless, typově bezpečné, framework-agnostické (React, Solid, Vue a další) a navržené tak, aby spolu dobře fungovaly. Když se naučíte jednu, ostatní vám přijdou povědomé. A když se rozhodnete jednu z nich nepoužít, nic se nerozbije.</p>
<p>Za ekosystémem navíc stojí silní partneři: Netlify, Vercel, Cloudflare, Railway, Render, Lovable, Clerk, WorkOS, Sentry, Prisma a další. To nejsou jen loga na webu. Jsou to SDK, šablony a integrace, které můžete použít hned.</p>
<h2 id="co-neni-dokonale">Co není dokonalé<a href="#co-neni-dokonale" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Abych nepsal jen chvalozpěv: Nitro v3, na kterém Start v produkci běží, je stále v betě. Funguje nám bez problémů, ale je dobré to vědět. A některé pokročilé věci (třeba RSC) se pořád vyvíjejí, takže pokud na nich chcete stavět celou aplikaci, počítejte s tím.</p>
<p>Ani jedno pro nás nebyl důvod Start odložit.</p>
<h2 id="jak-zacit">Jak začít<a href="#jak-zacit" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Pokud jste čekali na verzi 1.0, teď je ta chvíle:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">pnpm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> create</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> @tanstack/start@latest</span></span>
<span class="line"></span></code></pre>
<p>Pak doporučuju <a href="https://tanstack.com/start/latest">dokumentaci Startu</a>, a pokud migrujete z Next.js, má TanStack i průvodce migrací. A jak radí sám Tanner: pusťte na to klidně AI agenta. S aktuálními skills z balíčků, TypeScriptem a testy to zvládne překvapivě daleko.</p>
<h2 id="zaver">Závěr<a href="#zaver" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>TanStack Start 1.0 je pro mě potvrzení něčeho, co jsem věděl už dlouho. Framework nemusí diktovat, jak má vypadat celá aplikace. Může vám dát silné, typované a explicitní nástroje a nechat rozhodnutí na vás.</p>
<p>Na tomhle webu i na Udalistu to máme v produkci a všechno funguje, jak má. Kdo se mě zeptá, co dnes zvolit na frontend, dostane jasnou odpověď: <strong>TanStack Start. A k němu zbytek TanStacku.</strong></p>
<p>Díky, Tannere, a díky celému týmu kolem TanStacku. A pokud řešíte, jestli a jak na Start přejít, <a href="https://bhpslabs.com/cs/contact">ozvěte se nám</a>. Rádi poradíme.</p>]]></content:encoded>
    </item>
    <item>
      <title>AI slop a kód: Proč linter a formatter potřebujete víc než kdy dřív</title>
      <link>https://bhpslabs.com/cs/blog/lint-v-dobe-ai-slopu</link>
      <guid isPermaLink="true">https://bhpslabs.com/cs/blog/lint-v-dobe-ai-slopu</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Jan Szotkowski</dc:creator>
      <category>ai</category>
      <category>linting</category>
      <category>oxlint</category>
      <category>oxfmt</category>
      <category>eslint</category>
      <category>prettier</category>
      <category>coding-standards</category>
      <description>Pokud vám nevadí plýtvat penězi a časem, tenhle článek nečtěte. AI píše kód rychle, ale pokaždé jinak, a bez jasných pravidel se codebase rozpadne. Proč jsou Oxlint a Oxfmt v éře AI agentů tak důležité a proč záleží na rychlosti.</description>
      <content:encoded><![CDATA[<p><strong>Pokud vám nevadí platit vývojáře za čekání na terminál, platit CI minuty za pipeline, která běží o půl hodiny déle, než musí, a platit tokeny za to, že AI agent opravuje stejné chyby potřetí, tak tenhle článek nečtěte.</strong> Nic vám v něm nechybí. Peníze a čas jsou vám očividně šumák.</p>
<p>Všichni ostatní, pokračujte.</p>
<p>AI vám dnes napíše komponentu za minutu. Třeba i dvacet komponent za pět minut. Funguje to, testy projdou, všichni jsou spokojení. Pak otevřete pull request a vidíte, že jedna komponenta používá <code>interface</code>, druhá <code>type</code>, třetí má <code>console.log</code> zapomenutý uprostřed a čtvrtá importuje stejnou knihovnu třemi různými způsoby.</p>
<p>Žádná z těch věcí není chyba. Kód běží. Jenže codebase, ve které každý soubor vypadá, jako by ho psal někdo jiný, se špatně čte, špatně reviewuje a špatně udržuje. Té záplavě „nějak to funguje, ale nikdo to nesjednotil" se dnes říká <strong>AI slop</strong>. A každá hodina, kterou strávíte jeho úklidem, je hodina, kterou platíte.</p>
<p>Dobrá zpráva: dá se tomu předejít skoro zadarmo. Špatná zpráva: pokud to děláte pomalými nástroji, předejdete tomu jen napůl, protože je budete obcházet.</p>
<p>A důležitá věc na úvod: není to problém jen AI.</p>
<h2 id="neni-to-jen-o-ai-jenom-je-to-videt-vic">Není to jen o AI, jenom je to vidět víc<a href="#neni-to-jen-o-ai-jenom-je-to-videt-vic" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Už v článku o <a href="https://bhpslabs.com/cs/blog/interface-vs-type">pravidlu Interface vs. Type</a> jsem psal o tom, co se stane, když se tým rozroste. Deset vývojářů, deset trochu jiných zvyklostí. Někdo je zvyklý na <code>interface</code> z Javy, někdo míchá oba přístupy a code review se promění v diskusi o mezerách místo o architektuře.</p>
<p>AI agent je v podstatě dalších deset vývojářů, kteří se nikdy nepotkali. Nezná vaše „nepsané pravidlo". Každá session začíná od nuly, model má naučené tisíce různých stylů a vybere si ten, který se mu zrovna hodí. A navíc píše <strong>rychleji než kdokoli z nás</strong>, takže nekonzistence rostou rychleji, než je stihne kdokoli zachytit.</p>
<p>Odpověď je stejná jako dřív, jen s vyšší naléhavostí: <strong>pravidla musí hlídat stroj, ne lidská paměť.</strong></p>
<h2 id="pravidlo-v-promptu-je-prosba-pravidlo-v-linteru-je-zakon">Pravidlo v promptu je prosba, pravidlo v linteru je zákon<a href="#pravidlo-v-promptu-je-prosba-pravidlo-v-linteru-je-zakon" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Dá se samozřejmě napsat do <code>AGENTS.md</code> nebo do systémového promptu: „Používej <code>type</code> místo <code>interface</code>, nepoužívej <code>any</code>, nenechávej <code>console.log</code>." Často to funguje. Ale je to jen prosba. Model na ni zapomene, jakmile se kontext zaplní, nebo si ji vyloží po svém.</p>
<p>Lint pravidlo funguje jinak. Buď projde, nebo neprojde. Nediskutuje se.</p>
<p>Moderní AI agenti navíc pracují ve smyčce:</p>
<ol>
<li>napíšou kód,</li>
<li>spustí kontroly (testy, typy, lint),</li>
<li>přečtou chyby,</li>
<li>opraví je a jedou znovu.</li>
</ol>
<p>Linter a formatter jsou přesně ta zpětná vazba, kterou tahle smyčka potřebuje. Agent se nemusí domýšlet, co máte rádi. Spustí jeden příkaz a dostane konkrétní odpověď: tady řádek 42, tohle pravidlo, tohle se nesmí.</p>
<p>Formatter navíc eliminuje celou kategorii šumu. Agent (ani člověk) nemusí řešit uvozovky, odsazení nebo čárky. Diff v pull requestu obsahuje jen skutečné změny a reviewer se soustředí na to, co dělá kód, ne na to, jak vypadá.</p>
<h2 id="kratka-historie-eslint-a-prettier">Krátká historie: ESLint a Prettier<a href="#kratka-historie-eslint-a-prettier" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>ESLint vznikl v roce 2013 a Prettier v roce 2017. Změnily JavaScript svět. ESLint hlídal kvalitu kódu (nepoužité proměnné, nebezpečné konstrukce, vlastní týmová pravidla), Prettier odstranil věčné spory o formátování tím, že rozhodl za všechny.</p>
<p>Dvojice ESLint + Prettier se stala standardem a po deset let fungovala skvěle. Mělo to ale jednu vlastnost, se kterou jsme se naučili žít: oba nástroje jsou napsané v JavaScriptu a běží v Node.js. Na malém projektu to nikdo nevnímá. Na velké codebase s TypeScriptem a type-aware pravidly se z lintu stane něco, co pouštíte „až na konec", protože trvá minuty.</p>
<h2 id="rust-zmenil-pravidla-hry">Rust změnil pravidla hry<a href="#rust-zmenil-pravidla-hry" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Pak přišel projekt <a href="https://oxc.rs">Oxc</a> a s ním <strong>Oxlint</strong> (2023) a později <strong>Oxfmt</strong>. Obojí je napsané v Rustu, běží nativně a využívá všechna jádra procesoru.</p>
<p>Rozdíl v rychlosti není kosmetický. Jeden vývojář na Redditu popsal migraci svého firemního projektu (M3 MacBook Pro, medián ze tří běhů, přes 6 000 souborů):</p>
<ul>
<li>ESLint (single-thread, type-aware pravidla): <strong>~2 min 27 s</strong></li>
<li>Oxlint (5 360 souborů, 134 pravidel, 11 vláken): <strong>~1,3 s</strong></li>
<li>Prettier: <strong>~13,9 s</strong></li>
<li>Oxfmt: <strong>~2,1 s</strong></li>
</ul>
<p>Pozor, je to jeden konkrétní projekt s jednou konkrétní konfigurací a váš výsledek se bude lišit. Oficiální čísla z dokumentace Oxc jsou střízlivější: Oxfmt je zhruba 30× rychlejší než Prettier. Ale řád je všude stejný. Mluvíme o desítkách a stovkách násobků, ne o desítkách procent.</p>
<p>A tady je důležité, co se za poslední rok změnilo:</p>
<ul>
<li><strong>Oxfmt je od února 2026 v betě a prochází 100 % konformitních testů Prettieru</strong> pro JavaScript a TypeScript. Zbylé rozdíly jsou známé chyby samotného Prettieru. Výstup se tedy neliší, jen je rychlejší.</li>
<li><strong>Type-aware linting je od července 2026 stabilní.</strong> Dělá ho <code>tsgolint</code> postavený na nativním TypeScriptu 7 a pokrývá 59 z 61 type-aware pravidel z typescript-eslint. To byla dlouho největší výmluva, proč zůstat u ESLintu.</li>
<li>Oxlint dnes podporuje přes 800 pravidel z ESLint core a populárních pluginů (React, jsx-a11y, import, unicorn...) a umí spouštět i JS pluginy.</li>
</ul>
<h2 id="rychlost-jsou-penize">Rychlost jsou peníze<a href="#rychlost-jsou-penize" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Říkám to vždycky, i když to zní trochu suše: rychlost nástrojů je business argument.</p>
<p>Spočítejme si to na jednoduchém, ilustračním příkladu. Tým deseti vývojářů, každý spustí kontroly desetkrát denně (před commitem, před pushem, při opravách po CI). Při dvou a půl minutě čekání na jeden běh to dělá <code>10 × 10 × 2,5 min</code>, tedy zhruba <strong>4 hodiny denně</strong> a asi 80 hodin měsíčně strávených pohledem na terminál. S Oxlintem (1,3 s na běh) je to dohromady necelé dvě minuty denně.</p>
<p>A to jsme nezapočítali:</p>
<ul>
<li><strong>CI/CD pipeline.</strong> Každá minuta v pipeline se platí (runnery) i čeká (vývojář). Rychlejší lint znamená rychlejší zpětnou vazbu na pull requestu a rychlejší release.</li>
<li><strong>Pre-commit hooky.</strong> Hook, který trvá 40 sekund, lidé začnou obcházet přes <code>--no-verify</code>. Hook, který trvá sekundu, nikdo nevypíná.</li>
<li><strong>Doručování.</strong> Rychlejší cyklus znamená, že klientům dříve doručíme featuru i opravu chyby. To je to, na co se zákazník ptá.</li>
</ul>
<h2 id="kde-do-toho-zapada-ai">Kde do toho zapadá AI<a href="#kde-do-toho-zapada-ai" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Tady se ta rychlost násobí. Vývojář spustí lint párkrát za den. <strong>AI agent ho pouští po každé iteraci</strong>, tedy klidně dvacetkrát za jeden úkol.</p>
<p>Pokud každý běh trvá dvě minuty, agent buď čeká (a vy čekáte s ním, případně platíte minuty běhu cloudového agenta), nebo, a to je horší, kontrolu začne přeskakovat. Pomalá zpětná vazba vede k tomu, že agent udělá víc práce naslepo a pak opravuje víc věcí najednou.</p>
<p>Co se týče tokenů, buďme přesní: samotné čekání tokeny nepálí. Pálí je <strong>počet iterací a velikost kontextu</strong>. Když agent dostane rychlou a přesnou zpětnou vazbu hned po každé změně, opraví chybu, dokud je malá. Když ji dostane až na konci, musí znovu číst větší kus kódu, rozplétat souvislosti a opravovat i věci, které mezitím postavil na špatném základu. Podle toho, jaký model používáte, to může být znát i na faktuře.</p>
<p>Výsledek: <strong>rychlý linter je pro AI agenta to, co rychlé testy pro člověka.</strong> Dokud trvají vteřiny, používá se. Jakmile trvají minuty, začne se kolem nich chodit.</p>
<h2 id="jak-to-mame-nastavene-my">Jak to máme nastavené my<a href="#jak-to-mame-nastavene-my" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Nezůstaňme jen u teorie. Tenhle web běží na Oxlintu a Oxfmt. Konfigurace linteru vypadá takhle:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">import</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> { defineConfig } </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">from</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'oxlint'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">export</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> default</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> defineConfig</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">({</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    plugins: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'import'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'unicorn'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'react'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'typescript'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'jsx-a11y'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">]</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    categories: {</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        correctness: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'warn'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    }</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    rules: {</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">        'eslint/no-console'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'error'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, { allow: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'warn'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'error'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">] }],</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">        '@typescript-eslint/consistent-type-definitions'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'error'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'type'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">],</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">        '@typescript-eslint/no-explicit-any'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'error'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">        '@typescript-eslint/no-unused-vars'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'error'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    }</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">,</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">})</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"></span></code></pre>
<p>Všimněte si prostředního řádku. Pravidlo <code>consistent-type-definitions</code>, kvůli kterému jsem kdysi psal vlastní ESLint plugin, je dnes <strong>jeden řádek konfigurace</strong> a běží v Oxlintu nativně. Stejně tak pravidla pro přístupnost (<code>jsx-a11y</code>), která si AI ráda vynechá.</p>
<p>A abychom nemuseli myslet na tři příkazy, máme jeden v <code>package.json</code>:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "scripts"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: {</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "lint"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"oxlint"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "fmt"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"oxfmt"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "cq"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"oxlint &#x26;&#x26; oxfmt --check &#x26;&#x26; tsc --noEmit"</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    }</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span></code></pre>
<p><code>cq</code> (code quality) pouští lint, kontrolu formátu a typy. Stejný příkaz běží v CI. Stejný příkaz říkáme agentovi. Do instrukcí pro agenty (<code>AGENTS.md</code>, <code>CLAUDE.md</code>, rules) stačí jedna věta:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">Než práci označíš za hotovou, spusť </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">`pnpm run cq`</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> a oprav všechna zjištění.</span></span>
<span class="line"></span></code></pre>
<p>Tím se z „doporučení" stává brána, kterou kód musí projít, ať ho psal člověk, nebo model.</p>
<h2 id="neni-to-jeste-100-a-stejne-bych-nesahl-po-nicem-jinem">Není to ještě 100 % (a stejně bych nesáhl po ničem jiném)<a href="#neni-to-jeste-100-a-stejne-bych-nesahl-po-nicem-jinem" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Nechci tu psát reklamu, takže na rovinu: Oxlint a Oxfmt ještě nejsou 100% náhrada za ESLint a Prettier. Pár věcí, na které narazíte:</p>
<ul>
<li><strong>Ne každé pravidlo existuje.</strong> Oxlint pokrývá přes 800 pravidel, ale nějaké speciální pravidlo z ESLint pluginu, na kterém závisíte, může chybět.</li>
<li><strong>Vlastní ESLint pluginy.</strong> Lokální pluginy se nemigrují automaticky, ale dají se ručně přidat do konfigurace přes <code>jsPlugins</code>. Oxlint umí většinu ESLint v9 API, ne úplně všechno.</li>
<li><strong>Prettier pluginy a méně obvyklé formáty.</strong> Oxfmt je v betě a podpora Prettier pluginů se teprve dotahuje. Pokud závisíte na přesném chování konkrétního pluginu, ověřte si to předem.</li>
<li><strong>Type-aware linting vyžaduje TypeScript 7.</strong> Na starší verzi je to jeden krok navíc.</li>
</ul>
<p>Co s tím? Můj postoj je jednoduchý: <strong>dokud nepřijde něco lepšího, nesáhl bych po ničem jiném než po Oxlintu a Oxfmt.</strong> Ne proto, že jsou dokonalé, ale proto, že rozdíl v rychlosti je tak velký, že pár chybějících pravidel nebo drobných odchylek jde vyřešit, kdežto pomalý nástroj v pipeline nevyřešíte nikdy. Vývoj jde navíc rychle dopředu: Oxfmt prošel za pár měsíců od alfy k betě s plnou kompatibilitou na JS a TS a type-aware linting je stabilní.</p>
<p>Pokud vám opravdu nějaké pravidlo chybí, jde to přemostit tím, že Oxlint pustíte první a ESLint necháte jen pro to, co zbývá (<code>oxlint &#x26;&#x26; eslint</code>, plugin <code>eslint-plugin-oxlint</code> vypne duplicity). Berte to ale jako dočasnou lávku, ne jako cílový stav.</p>
<h2 id="jak-zacit-za-odpoledne">Jak začít (za odpoledne)<a href="#jak-zacit-za-odpoledne" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Migrace je dnes překvapivě bezbolestná.</p>
<p>Linter:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">pnpm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> add</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> -D</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> oxlint</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">npx</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> @oxlint/migrate</span></span>
<span class="line"></span></code></pre>
<p>Formatter:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">pnpm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> add</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> -D</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> oxfmt@latest</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">pnpm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> oxfmt</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> --migrate=prettier</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">pnpm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> oxfmt</span></span>
<span class="line"></span></code></pre>
<p>Hromadné přeformátování nechte jako samostatný commit a přidejte jeho hash do <code>.git-blame-ignore-revs</code>, aby <code>git blame</code> nepoukazoval na „kdo přeformátoval celý projekt". Potom aktualizujte CI a pre-commit hooky a nakonec napište příkaz do instrukcí pro AI agenty.</p>
<h2 id="zaver">Závěr<a href="#zaver" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Ve světě, kde kód z velké části píše model, není konzistence luxus. Je to jediná věc, která udrží codebase čitelnou pro lidi i pro další AI agenty, kteří po vás přijdou.</p>
<p>Pravidla musí být <strong>automatická</strong> (nechceme na ně pořád myslet), <strong>vynucená</strong> (žádné „příště si dám pozor") a <strong>rychlá</strong> (jinak se jim všichni vyhnou). ESLint a Prettier nás tomu naučili. Oxlint a Oxfmt to dotáhly do stavu, kdy kontrola trvá vteřinu, ne minuty. A to se vyplatí, ať už platíte za čas vývojářů, za CI minuty, nebo za tokeny.</p>
<p>Pokud je váš lint dnes ten krok, který pouštíte „až na konec", zkuste ho přepnout. Stačí odpoledne. A pak se mi ozvěte, kolik sekund vám zbylo.</p>]]></content:encoded>
    </item>
    <item>
      <title>Od elektrikáře k Tech leadovi: Z montérek k monitoru</title>
      <link>https://bhpslabs.com/cs/blog/od-elektrikare-k-tech-leadovi</link>
      <guid isPermaLink="true">https://bhpslabs.com/cs/blog/od-elektrikare-k-tech-leadovi</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Jan Szotkowski</dc:creator>
      <category>career</category>
      <category>frontend</category>
      <category>mentoring</category>
      <description>Před sedmi lety jsem stál uprostřed továrny s rukama špinavýma od oleje a pocitem, že můj život nikam nevede. Dnes, ve 26 letech, jsem front-end tech lead. Nic mi nikdo nedal zadarmo.</description>
      <content:encoded><![CDATA[<h2 id="uvod">Úvod<a href="#uvod" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Před sedmi lety jsem stál uprostřed továrny – ruce špinavé od oleje, montérky plné prachu, kolem mě hluk strojů a v hlavě pocit, že můj život nikam nevede. Směny se táhly jedna za druhou, a když pršelo, sedl jsem na kolo a šlapal domů – promočený, unavený, s myšlenkou, že takhle to asi bude navždy. Nebyl jsem zoufalý, ale cítil jsem, že chci víc. Ne proto, že bych se považoval za lepšího než lidi kolem mě – ti byli poctiví, pracovití, stejně jako já. Nikdy jsem nebyl jedničkář ve škole, nebyl jsem nějaký výjimečný talent. Byl jsem obyčejný kluk, co chtěl v životě něco dokázat. Jednoho dne jsem se zastavil v dešti, podíval se na sebe – zašpiněný elektrikář na kole – a řekl si: „Tohle nechci dělat do konce života.“ Touha po změně mě přivedla k programování – k něčemu, o čem jsem nikdy nesnil a k čemu jsem neměl žádné předpoklady. Dnes, ve 26 letech, jsem front-end tech lead v Raynet CRM, jednom z nejlepších CRM systémů v Česku a na Slovensku. Nic mi nikdo nedal zadarmo – všechno, co mám, jsem si vydřel sám. Neměl jsem to lehké, stejně jako nikdo v továrně, a nechci, aby to znělo, že jsem něco víc. Jen jsem se rozhodl jít za svým cílem, i když jsem si na začátku vůbec nevěřil.</p>
<h2 id="zacatky-zadne-predpoklady-zadna-pomoc">Začátky: Žádné předpoklady, žádná pomoc<a href="#zacatky-zadne-predpoklady-zadna-pomoc" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Nikdy jsem si nemyslel, že budu programátor. Jako dítě jsem nebyl ten kluk, co tráví hodiny u počítače nebo skládá roboty. Běhal jsem venku, hrál fotbal, bavil mě reálný svět – technologie mě míjely. Po škole jsem se stal elektrikářem, protože to bylo praktické a dostupné. Uměl jsem zapojit kabely, spravit zásuvku, ale o programování jsem nevěděl nic. Život v továrně byl rutina – vstávat, makat, domů, spát, a zase dokola. Lidi kolem mě byli skvělí, pracovití, a já jsem je respektoval. Nešlo o to být lepší než oni – jen jsem cítil, že chci něco jiného. Koupil jsem si starý notebook – takový ten, co vrčel jako traktor a po chvíli se přehříval – a začal hledat na internetu, co je to programování. Neměl jsem nikoho, kdo by mě nasměroval – žádného kamaráda ani známého v oboru. První večery byly chaos – seděl jsem po směně, oči mě pálily z únavy, a snažil jsem se pochopit, co znamená „Hello World“ v HTML. Připadalo mi to jako cizí svět. Nevěřil jsem si, ale řekl jsem si: „Když to zvládli jiní, zvládnu to taky.“ A tak jsem začal.</p>
<h2 id="disciplina-jako-jediny-spolecnik">Disciplína jako jediný společník<a href="#disciplina-jako-jediny-spolecnik" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Každý den jsem si vyhradil čas. Po směně, když jsem přišel domů špinavý a vyčerpaný, jsem si řekl: „Aspoň hodinu.“ Někdy jsem vstával ve čtyři ráno, ještě za tmy, abych stihl tutoriál před prací. Bylo to, jako bych měl dvě zaměstnání – v továrně jsem šrouboval a opravoval, doma jsem se učil něco nového. Hledal jsem odpovědi na Google, procházel YouTube videa, četl fóra do noci. Když jsem poprvé pochopil CSS a viděl, jak se stránka mění podle mého kódu, cítil jsem naději. Pak přišel JavaScript – úplně jiný svět, hodiny jsem zíral na chyby v konzoli a nerozuměl, proč to nefunguje. Často jsem chtěl skončit. Měl jsem impostor syndrom – pocit, že jsem podvodník, co na to nemá. Ale motivace něčeho dosáhnout mě hnala dál. Chtěl jsem být hrdý na sebe – na to, že to zvládnu vlastními silami, že mi to nikdo nemůže vzít. Když se mi podařilo rozjet jednoduchou kalkulačku nebo animaci, říkal jsem si: „Možná na tom něco je.“ Disciplína mě držela nad vodou – i když jsem si nevěřil, nevzdal jsem to.</p>
<h2 id="cesta-plna-zklamani">Cesta plná zklamání<a href="#cesta-plna-zklamani" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Po měsících učení jsem se rozhodl zkusit práci. Napsal jsem životopis – obyčejný, bez titulů, jen s pár větami o tom, co jsem zvládl sám. Rozeslal jsem ho – nejdřív deset, pak dvacet, nakonec asi sto. A co přišlo? Ticho. Občas e-mail: „Děkujeme, ale hledáme někoho s praxí.“ Nebo: „Bez školy to nejde.“ Jako junior s minulostí elektrikáře jsem byl neviditelný. Ty chvíle mě deptaly. Seděl jsem večer doma, zíral na další odmítnutí a ptal se sám sebe: „Proč to dělám?“ Impostor syndrom sílil – cítil jsem se jako podvodník, co si jen hraje na programátora. Jednou jsem seděl v kuchyni, venku lilo, a říkal si: „Možná jsem blázen.“ Chtěl jsem to vzdát, vrátit se k montérkám, k jistotě. Ale pak mě napadlo, co by to znamenalo – žádný pokrok, jen továrna. Chtěl jsem něco, na co budu moct být hrdý. Tak jsem se zvedl, otevřel další tutoriál, poslal další životopis. Po šesti měsících – plných odmítnutí a pochybností – mi někdo zavolal. „Máme pozici junior programátora v Pickering Interfaces v Třinci. Chcete to zkusit?“ Ten hovor změnil všechno.</p>
<h2 id="prvni-prace-nechteny-projekt-a-mezinarodni-tym">První práce: Nechtěný projekt a mezinárodní tým<a href="#prvni-prace-nechteny-projekt-a-mezinarodni-tym" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Když jsem nastoupil do Pickering Interfaces v Třinci, dceřiné společnosti britského Pickeringu, cítil jsem, že se můj život posouvá. Poprvé jsem měl kolegy – a ne jen tak ledajaké. Pracoval jsem v mezinárodním týmu na projektu, který přesahoval hranice Česka. Po měsících osamělého učení to bylo osvobozující, ale ne jednoduché. Přišel jsem jako náhrada za programátora, který odešel, a zdědil jsem projekt, co nikdo nechtěl – zapomenutý kód, který nikdo nechtěl spravovat ani vyvíjet. Jako nováček jsem dostal to, co bývá zvykem – tu nejtěžší, nejméně oblíbenou práci. Ze začátku jsem nevěděl, kde začít. Impostor syndrom mě pronásledoval – bál jsem se, že selžu. Ale řekl jsem si: „Já jsem jiný. Naučím se to.“ Pustil jsem se do toho – učil jsem se za pochodu, řešil chyby, hledal cestu. Postupně jsem ten projekt zvládl, a dnes jsem za to rád. Nebyli na mě měkcí, hodili mě do hluboké vody, a to mě donutilo růst. Dokonce jsem za svou práci dostal sklenku na whisky od britské královny – maličkost, ale pro mě symbol, že tvrdá práce má smysl. Po dvou letech v Třinci jsem musel opustit rodné město a přestěhovat se do Ostravy – těžké rozhodnutí, ale věděl jsem, že mě čeká další výzva.</p>
<h2 id="rust-v-raynet-crm-od-juniora-k-tech-leadovi">Růst v Raynet CRM: Od juniora k tech leadovi<a href="#rust-v-raynet-crm-od-juniora-k-tech-leadovi" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>V Ostravě jsem nastoupil do Raynet CRM jako junior programátor. Začínal jsem odspoda, ale poprvé jsem měl kolegy a mentory, kteří mi pomohli, kdykoliv jsem potřeboval. Žádná otázka nebyla hloupá – jak jsem si řekl: „Ten, kdo se bojí zeptat, je trouba navždy; ten, kdo se nebojí, je trouba jen na pár minut.“ Ptát jsem se nebál. Po letech bez podpory to byla změna – měl jsem tým, co mě táhl dopředu. Mentoři mě učili psát lepší kód a přemýšlet jako programátor. Disciplína, kterou jsem si vybudoval, se spojila s jejich vedením, a já jsem rostl rychle. Za čtyři roky jsem se z juniora stal front-end tech lead – ve 26 letech! Dnes navrhuju řešení a pracuju na špičkovém CRM. Moji nadřízení mi důvěřují, můj názor má váhu, a já mám vliv na fungování týmu. Jsem hrdý – nejen za sebe, ale i za to, že můžu pomoci rodině a přátelům, kdykoliv mě potřebují. Práce hlavou je náročná, někdy mi chybí ta obyčejná práce rukama – šroubování, opravování, něco hmatatelného. Ale vím, že to, co dělám teď, má smysl. Před pár měsíci jsem napsal kus kódu, který teď programátoři po celém světě můžou využít – a využívají ho. Když to vidím, cítím, že má práce přesahuje můj stůl.</p>
<h2 id="kam-dal-vize-a-mentoring">Kam dál: Vize a mentoring<a href="#kam-dal-vize-a-mentoring" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Když jsem byl elektrikář, nevěřil jsem, že ve 26 letech budu tech lead. Teď si říkám: „Uvidíme, kam mě cesta ještě zavede.“ Chci růst dál – v technologiích i v leadershipu. Sním o větším vlivu – třeba vést větší tým, podílet se na přelomových projektech, nebo zkusit podnikání v IT. Možná jednou vytvořím vlastní aplikaci, která pomůže lidem, jako Raynet pomáhá teď. Každý den se učím něco nového – ať je to framework, nebo jak motivovat tým. Teď jsem já mentorem pro své juniory v Raynetu, a to mě baví. Vidím v nich sebe – ty pochybnosti, ten strach, jestli na to mají. Pomáhám jim, jak jsem kdysi potřeboval pomoc já, a vidět jejich pokroky mě motivuje dál. Cesta mě naučila, že limity si stavíme sami – a já je stavět nechci.</p>
<h2 id="zaver-ty-na-to-taky-mas">Závěr: Ty na to taky máš<a href="#zaver-ty-na-to-taky-mas" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Můj příběh není o talentu. Nebyl jsem génius, neměl jsem předpoklady, dlouho jsem neměl nikoho, kdo by mi pomohl. Nejsem jiný než lidi v továrně nebo kdokoli, kdo dělá práci, co ho nebaví. Nejsem lepší – jen jsem se rozhodl vystoupit z komfortní zóny, i když jsem si nevěřil. Na začátku jsem měl impostor syndrom, nevěřil jsem, že na to mám, chtěl jsem skončit. Ale nevzdal jsem to. Od továrny přes Pickering Interfaces v Třinci, kde jsem pracoval pro britskou firmu, až po Raynet CRM v Ostravě – z nuly na Tech leada ve 26 letech. Jsem hrdý, že to, co jsem dokázal, je moje, a že můžu podpořit rodinu a přátele. Lidé často nevystupují z komfortní zóny, myslí si, že na změnu nemají. Já jsem byl stejný. Ale chci vám říct: Máte na to. Nejlepší čas začít byl třeba před rokem – druhý nejlepší je teď. Nemusíte mít předpoklady – já jsem je neměl. Stačí začít, vydržet a nevzdát se. Já jsem to dokázal – a vy to zvládnete taky. Udělejte ten první krok. Stojí to za to.</p>]]></content:encoded>
    </item>
    <item>
      <title>NPM Apokalypsa 2025: Jak klidně spát, když se hroutí ekosystém</title>
      <link>https://bhpslabs.com/cs/blog/npm-apokalypsa-2025</link>
      <guid isPermaLink="true">https://bhpslabs.com/cs/blog/npm-apokalypsa-2025</guid>
      <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Jan Szotkowski</dc:creator>
      <category>npm</category>
      <category>security</category>
      <category>renovate</category>
      <category>dependencies</category>
      <category>vulnerabilities</category>
      <description>Rok 2025 byl pro bezpečnost NPM balíčků brutální. Stovky napadených knihoven, miliony stažení malwaru. Jak se bránit? Ručně to nezvládnete. Představuji vám mou strategii s Renovate botem.</description>
      <content:encoded><![CDATA[<p>Pokud sledujete dění ve světě JavaScriptu, nemohlo vám to uniknout. Rok 2025 byl pro bezpečnost NPM ekosystému naprostá katastrofa.</p>
<p>Nebavíme se o jednotkách případů. Bavíme se o <strong>stovkách balíčků</strong>, které byly kompromitovány. Útočníci získali přístup k účtům maintainerů (často přes sociální inženýrství nebo uniklé tokeny) a do legitimních, hojně používaných knihoven propašovali škodlivý kód.</p>
<p>Některé z těchto balíčků měly miliony stažení týdně. Představte si to: v pondělí ráno přijdete do práce, spustíte <code>npm install</code>, a aniž byste cokoliv tušili, stahujete si do projektu malware, který krade vaše ENV proměnné nebo injectuje kryptominer do prohlížečů vašich uživatelů.</p>
<p>Je to děsivé. A upřímně – pokud spoléháte jen na štěstí, je to jen otázka času, kdy to trefí i vás.</p>
<h2 id="tri-vrstvy-manualni-obrany">Tři vrstvy manuální obrany<a href="#tri-vrstvy-manualni-obrany" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>První instinktivní reakce je: <em>"Přestanu používat NPM!"</em>
To je sice hezká myšlenka, ale v moderním frontend vývoji asi tak reálná, jako chtít postavit mrakodrap jen pomocí kladiva a hřebíků. Bez ekosystému Reactu, TanStacku nebo Tailwindu se dnes neobejdeme.</p>
<p>Existují ale kroky, které dělám, abych riziko minimalizoval:</p>
<h3 id="1-minimalismus-a-forkovani">1. Minimalismus a Forkování<a href="#1-minimalismus-a-forkovani" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Než přidám novou závislost, ptám se: <em>"Opravdu potřebuji celou knihovnu na to, abych naformátoval datum?"</em>
Pokud je to malá utilita, raději si ji napíšu sám. Pokud je to složitější knihovna, která ale obsahuje spoustu balastu, často udělám <strong>fork</strong>. Vezmu si jen tu část kódu, kterou potřebuji, a vložím ji přímo do svého repozitáře. Tím odstřihnu pupeční šňůru od budoucích aktualizací (které mohou být infikované).</p>
<h3 id="2-zakaz-skriptu-ignore-scripts">2. Zákaz skriptů (<code>ignore-scripts</code>)<a href="#2-zakaz-skriptu-ignore-scripts" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Většina útoků se spoléhá na tzv. <code>postinstall</code> skripty. To jsou příkazy, které se spustí automaticky hned po stažení balíčku. Útočníci je milují – je to ideální místo, kam schovat <code>curl</code>, který odešle vaše <code>.env</code> soubory na jejich server.</p>
<p>Obrana je jednoduchá. Do kořene projektu (k souboru <code>package.json</code>) přidejte soubor <code>.npmrc</code> s tímto obsahem:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">ignore-scripts</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">=true</span></span>
<span class="line"></span></code></pre>
<p>Tím globálně zakážete spouštění těchto skriptů. Ano, občas to rozbije instalaci nějakého legitimního nástroje (třeba esbuild nebo cypress), ale to se dá vyřešit whitelistováním konkrétních balíčků. Bezpečnost za to stojí.</p>
<h3 id="3-pnpm-a-verze-ktere-uz-chvili-ziji">3. pnpm a verze, které už chvíli žijí<a href="#3-pnpm-a-verze-ktere-uz-chvili-ziji" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>npm stáhne balíček v minutě, kdy ho někdo publikuje. Útočník vydá verzi, vy v tu samou chvíli spustíte instalaci, a je to u vás.</p>
<p>pnpm od verze 12 to v základu nedělá. Má pravidlo <code>minimumReleaseAge</code>: stáhne jen verzi, která je na registry starší než 24 hodin. Počítá se to v minutách, výchozí hodnota je <code>1440</code> (jeden den). Většina těch napadených releasů se odhalí a z registry zmizí právě během prvních hodin. Den prodlevy vás od nich odřízne dřív, než si je stáhnete.</p>
<p>Mně je den málo. Chci verze staré 7 až 14 dní, které už chvíli žijí a nikdo je nestačil stáhnout jako malware. Nastaví se to v <code>pnpm-workspace.yaml</code>. Sedm dní je <code>10080</code> minut, čtrnáct je <code>20160</code>:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC"># 7 dní. Když projekt může počkat dva týdny, dejte sem 20160.</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">minimumReleaseAge</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">10080</span></span>
<span class="line"></span></code></pre>
<p>Platí to i na tranzitivní závislosti, ne jen na to, co máte vypsané v <code>package.json</code>. Novější verzi si pořád můžete doinstalovat, až na ni přijde řada. Jen už to nebude ta, která vyšla před deseti minutami.</p>
<h2 id="automatizace-kdyz-clovek-nestaci">Automatizace: Když člověk nestačí<a href="#automatizace-kdyz-clovek-nestaci" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Manuální kroky jsou fajn, ale mají jednu slabinu: mě. Já zapomínám. Já nemám čas každé ráno číst bezpečnostní reporty a kontrolovat, jestli verze <code>lodash</code> 4.17.21 není náhodou děravá.</p>
<p>Dlouho jsem hledal nástroj, který by mi hlídal záda. Potřeboval jsem něco, co:</p>
<ol>
<li>Běží automaticky (CI/CD).</li>
<li>Zná databázi zranitelností (GitHub Security Advisories, OSV).</li>
<li>Nebude mě otravovat zbytečnými aktualizacemi, ale bude křičet, když jde o bezpečnost.</li>
</ol>
<p>Vítězem se stal Renovate Bot.</p>
<h2 id="muj-security-first-config">Můj "Security-First" Config<a href="#muj-security-first-config" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Renovate většina lidí zná jako nástroj, který automaticky zvedá verze balíčků. "Vyšla nová verze Reactu? Tady máš PR."</p>
<p>Já ho ale používám jinak. Na stabilních projektech nechci, aby mi bot "rozbíjel" aplikaci aktualizacemi minor verzí, které nepotřebuji. Chci, aby mlčel a ozval se pouze v případě, že moje aktuální verze obsahuje bezpečnostní díru.</p>
<p>Tohle je konfigurace, kterou aktuálně testuji a která mi dává klidné spaní:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "$schema"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"[https://docs.renovatebot.com/renovate-schema.json](https://docs.renovatebot.com/renovate-schema.json)"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "extends"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"group:turboMonorepo"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">":semanticCommits"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">],</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "timezone"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"Europe/Prague"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "prConcurrentLimit"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">10</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "prHourlyLimit"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "dependencyDashboard"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "dependencyDashboardApproval"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">false</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">    // Tady začíná magie: Zapínáme Security Alerts</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "vulnerabilityAlerts"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: {</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "enabled"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "labels"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"security"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"vulnerability"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">]</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    },</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "osvVulnerabilityAlerts"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">true</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "automerge"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">false</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "rangeStrategy"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"pin"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "schedule"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"at any time"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">],</span></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">    // Pravidlo, které vypíná VŠECHNY aktualizace...</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "packageRules"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        {</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">            "description"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"Disable all non-security updates"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">            "matchUpdateTypes"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"major"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"minor"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"patch"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"pin"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"digest"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"lockFileMaintenance"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"rollback"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"bump"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"replacement"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">],</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">            "enabled"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">false</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        }</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    ]</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span></code></pre>
<h3 id="jak-to-funguje">Jak to funguje?<a href="#jak-to-funguje" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Všimněte si sekce packageRules. Ta explicitně říká: "Zakaž (enabled: false) všechny typy aktualizací (major, minor, patch...)."</p>
<p>Kdybych skončil tady, Renovate by nedělal nic. Ale protože mám nahoře zapnuté vulnerabilityAlerts a osvVulnerabilityAlerts, Renovate stále kontroluje databáze zranitelností.</p>
<p>Výsledek?</p>
<ul>
<li>Vyjde axios v1.8.0 (nová feature)? Ticho. Renovate mě neruší.</li>
<li>Zjistí se, že axios v1.7.0 má bezpečnostní díru? ALARM. Renovate okamžitě vytvoří Pull Request s opravou na bezpečnou verzi a otaguje ho jako security.</li>
</ul>
<p>Tento config stačí zapojit do GitHub/GitLab CI pipeline a nastavit spouštění jednou denně (nebo každou hodinu).</p>
<p>Díky tomu vím, že i když já spím, můj repozitář je porovnáván s nejnovějšími databázemi hrozeb. Není to 100% ochrana (ta neexistuje), ale je to mnohem lepší, než doufat, že se mi to vyhne.</p>]]></content:encoded>
    </item>
    <item>
      <title>Interface vs. Type: Proč jsem vytvořil vlastní pravidlo ESLint (a jak to změnilo náš tým)</title>
      <link>https://bhpslabs.com/cs/blog/interface-vs-type</link>
      <guid isPermaLink="true">https://bhpslabs.com/cs/blog/interface-vs-type</guid>
      <pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate>
      <dc:creator>Jan Szotkowski</dc:creator>
      <category>typescript</category>
      <category>eslint</category>
      <category>interface</category>
      <category>type</category>
      <category>coding-standards</category>
      <description>Jako front-end vývojář s více než šesti lety zkušeností jsem zažil už pěknou řádku debat o TypeScriptu. Jedna, která vždy vyvolá vášnivé diskuze, je klasické dilema Interface vs. Type.</description>
      <content:encoded><![CDATA[<p>Jako front-end vývojář s více než šesti lety zkušeností jsem zažil už pěknou řádku debat o TypeScriptu. Jedna, která vždy vyvolá vášnivé diskuze, je klasické dilema „Interface vs. Type“. Na začátku své kariéry jsem o tom moc nepřemýšlel – zdálo se, že obojí splní svůj účel. Ale jak projekty rostly a týmy se rozšiřovaly, uvědomil jsem si, že malá rozhodnutí jako toto mohou rozhodnout o konzistenci kódu (nebo ji rozbít). To je důvod, proč jsem nakonec vytvořil vlastní pravidlo ESLint, abych vynutil používání type aliasů namísto deklarací interface v naší TypeScript codebase. V tomto článku se podělím o to, proč jsem se tak rozhodl, jak jsem pravidlo vytvořil a jak to proměnilo naše pracovní postupy. Navíc přihodím pár tipů, abyste si to mohli vyzkoušet sami.</p>
<h2 id="chaos-pri-skalovani-tymu-bez-jasnych-pravidel">Chaos při škálování týmu bez jasných pravidel<a href="#chaos-pri-skalovani-tymu-bez-jasnych-pravidel" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Když jsem ve své firmě začal pracovat s TypeScriptem, náš tým byl malý – jen hrstka vývojářů. Měli jsme ústní dohodu, že se budeme držet type aliasů místo deklarací interface, protože nám připadaly předvídatelnější (více o tom později). Zpočátku to fungovalo skvěle. Byli jsme sladění, náš kód byl čistý a život byl fajn. Pak ale náš tým rychle vyrostl. Během roku jsme měli 10 vývojářů přispívajících do stejné codebase, a tehdy se věci zkomplikovaly.</p>
<p>Noví členové týmu si nebyli vždy vědomi našeho „nepsaného pravidla“. Někteří preferovali interface, protože jim to připadalo povědomější z jejich Java backgroundu. Jiní míchali oba přístupy, aniž by si toho vůbec všimli. Code reviews se změnily v nekonečné debaty o stylu spíše než o podstatě. Bylo jasné, že potřebujeme lepší způsob, jak vynutit konzistenci – něco automatizovaného, spolehlivého a škálovatelného. Takže jsem se rozhodl vyhrnout si rukávy a vytvořit vlastní ESLint plugin: <code>eslint-plugin-interface-to-type</code>. Ten by automaticky vynucoval použití type namísto interface napříč celou naší codebase a zachránil nás před námi samými.</p>
<h2 id="proc-jsem-si-vybral-type-misto-interface">Proč jsem si vybral type místo interface<a href="#proc-jsem-si-vybral-type-misto-interface" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Než se ponoříme do technických detailů, pojďme si říct, proč jsem si vůbec vybral <code>type</code> místo <code>interface</code>. Na první pohled se mohou zdát zaměnitelné – oba definují tvary objektů, že? Ale je tu jemný rozdíl, který vás může ve větších projektech kousnout: Declaration Merging (slučování deklarací).</p>
<p>V TypeScriptu platí, že pokud deklarujete dvě definice interface se stejným názvem, automaticky se sloučí do jedné. Zde je rychlý příklad:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">interface</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> User</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    id</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> number</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">interface</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> User</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    name</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> string</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#56606a;--shiki-dark:#BDC4CC">// TypeScript merges them into:</span></span>
<span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">interface</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> User</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    id</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> number</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    name</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> string</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span></code></pre>
<p>Toto chování může být v některých případech užitečné, například při rozšiřování typů knihoven třetích stran. Ale v prostředí spolupráce je to recept na zmatek. Představte si dva vývojáře, kteří nevědomky deklarují konfliktní vlastnosti v oddělených souborech – je to chyba čekající na to, až se stane. Na druhou stranu, <code>type</code> aliasy se neslučují. Pokud se pokusíte deklarovat dva <code>type</code> aliasy se stejným názvem, TypeScript vyhodí chybu kompilace, což vás donutí vyřešit konflikt předem. Pro mě je tato předvídatelnost záchranou života.</p>
<p>Kromě slučování deklarací jsou type aliasy flexibilnější. Mohou reprezentovat uniony, průniky (intersections) a primitiva – věci, se kterými <code>interface</code> zápasí. I když <code>interface</code> má své místo (například když potřebujete rozšiřovat třídy), zjistil jsem, že <code>type</code> je bezpečnější výchozí volbou pro většinu našich případů použití.</p>
<h2 id="tvorba-pravidla-eslint-od-chaosu-ke-konzistenci">Tvorba pravidla ESLint: Od chaosu ke konzistenci<a href="#tvorba-pravidla-eslint-od-chaosu-ke-konzistenci" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Teď, když jsem měl jasný důvod pro vynucení type, potřeboval jsem nástroj, který by to zajistil automaticky. Tady přichází na řadu můj ESLint plugin, <code>eslint-plugin-interface-to-type</code>. Chtěl jsem pravidlo, které by označilo jakoukoli deklaraci interface, navrhlo její nahrazení za type alias a dokonce poskytlo možnost automatické opravy (autofix) pro zefektivnění procesu.</p>
<p>Tvorba vlastního pravidla ESLint byla zpočátku trochu děsivá – nikdy předtím jsem žádné nepsal. Ale po nějakém průzkumu jsem tomu přišel na kloub. Plugin využívá AST (Abstract Syntax Tree) ESLintu k detekci deklarací interface a jejich transformaci na ekvivalentní type aliasy. Například toto:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">interface</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> User</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    id</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> number</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    name</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> string</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    role</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'admin'</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> |</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'user'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span></code></pre>
<p>se označí a lze to automaticky opravit na:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">type</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757"> User</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> {</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    id</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> number</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    name</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> string</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">    role</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">:</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'admin'</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> |</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 'user'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">};</span></span>
<span class="line"></span></code></pre>
<p>Nejtěžší částí bylo řešení okrajových případů – jako jsou deklarace interface, které rozšiřují (extend) jiné interfacy nebo mají složité generiky. Strávil jsem hodiny laděním pravidla, abych zajistil kompatibilitu a vyhnul se falešným pozitivům. Další výzvou byla implementace funkce automatické opravy. Možnost --fix v ESLintu vyžaduje poskytnutí přesných transformací, takže jsem musel pečlivě mapovat syntaxi interface na syntaxi type, aniž bych rozbil kód.</p>
<p>Jakmile byl plugin hotový, jeho nastavení bylo přímočaré. Stačí jej nainstalovat jako dev dependency:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">npm</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> install</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> eslint-plugin-interface-to-type</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> --save-dev</span></span>
<span class="line"></span></code></pre>
<p>Poté jej přidejte do své konfigurace ESLint:</p>
<pre class="shiki shiki-themes github-light-high-contrast github-dark-high-contrast" style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0"><code><span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "plugins"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"interface-to-type"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">],</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">    "rules"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: {</span></span>
<span class="line"><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">        "interface-to-type/prefer-type-over-interface"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"error"</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    }</span></span>
<span class="line"><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span>
<span class="line"></span></code></pre>
<p>Spusťte ESLint s --fix a on automaticky převede vaše deklarace interface na type aliasy. Hotovo!</p>
<h2 id="dopad-na-nase-pracovni-postupy-mene-bojovani-vice-staveni">Dopad na naše pracovní postupy: Méně bojování, více stavění<a href="#dopad-na-nase-pracovni-postupy-mene-bojovani-vice-staveni" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Dopad tohoto pravidla na náš tým byl okamžitý. Dříve jsme trávili kusy code reviews dohadováním se o interface vs. type. Nyní za nás pravidlo vynucuje konzistenci a uvolňuje mentální prostor pro důležitější diskuze – jako je architektura nebo optimalizace výkonu.</p>
<p>Také nám to ušetřilo čas. Noví vývojáři už nepotřebovali dlouhou úvodní přednášku o našich preferencích stylu. Pravidlo to prostě vyřešilo. Code reviews se zrychlily, protože jsme už nehledali hnidy v syntaxi. Je to malá změna, ale dominový efekt byl obrovský.</p>
<h2 id="tipy-pro-vyvojare-jak-zaridit-aby-to-fungovalo-pro-vas-tym">Tipy pro vývojáře: Jak zařídit, aby to fungovalo pro váš tým<a href="#tipy-pro-vyvojare-jak-zaridit-aby-to-fungovalo-pro-vas-tym" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Pokud vás tato myšlenka zaujala, zde je několik tipů, které vám pomohou vynutit používání type – nebo jakéhokoli standardu kódování – ve vašich projektech:</p>
<ol>
<li>Nejprve pochopte své potřeby: Než začnete vynucovat type namísto interface, ujistěte se, že to odpovídá cílům vašeho týmu. Pokud se silně spoléháte na Declaration Merging nebo implementace tříd, interface může mít stále své místo. Pro nás type fungoval lépe v 99 % případů, takže dávalo smysl se na něm sjednotit.</li>
<li>Automatizujte pomocí nástrojů jako ESLint: Nespoléhejte na ústní dohody – ty se neškálují. Nástroje jako ESLint (nebo Prettier pro formátování) mohou konzistentně vynucovat pravidla v celém týmu. Psaní vlastního pravidla může znít zastrašujícím dojmem, ale je to skvělá zkušenost k učení a z dlouhodobého hlediska se vyplatí.</li>
<li>Podporujte zapojení týmu (Buy-In): Pokud zavádíte nový standard, získejte nejprve zpětnou vazbu od kolegů. Před zavedením jsem své pravidlo ESLint sdílel s týmem a jejich vstupy mi pomohly zachytit několik okrajových případů, které jsem přehlédl.</li>
</ol>
<h2 id="vyzkousejte-to-sami-a-dejte-mi-vedet">Vyzkoušejte to sami a dejte mi vědět!<a href="#vyzkousejte-to-sami-a-dejte-mi-vedet" class="heading-anchor" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Přechod na type aliasy a jejich vynucení pomocí vlastního pravidla ESLint byl pro můj tým zásadní změnou. Náš kód se stal předvídatelnějším, recenze produktivnějšími a onboarding hladším. Pokud řešíte podobné problémy – nebo jen chcete experimentovat s přísnějšími standardy – doporučuji vám to vyzkoušet.</p>
<p>Začněte instalací <code>eslint-plugin-interface-to-type</code> a jeho spuštěním na vaší codebase. Uvidíte, jaký je to pocit mít konzistentní přístup k typům v TypeScriptu. Máte jiný pohled na interface vs. type? Nebo pravidlo odhalilo ve vašem projektu nějaké nečekané problémy?</p>]]></content:encoded>
    </item>
  </channel>
</rss>
