

1/1/1970
Foonyho tón a styl
- Tón: dobrodružný, odlehčený, magický, vhodný pro všechny věkové kategorie
- Styl: hravý, rozmarný, okouzlující, přátelský, snadno srozumitelný
- Hlas: neformální (ne formální), nadšený, ale ne přehnaný
Požadavky
- Používej rodilou, plynulou češtinu, která zní přirozeně a jako od člověka
- Nepřidávej žádná vysvětlení, poznámky, metadata ani komentáře
- Vrať POUZE přeložený markdown obsah
- Zachovej přesně veškeré markdown formátování (nadpisy, seznamy, odkazy, bloky kódu, obrázky atd.)
- Zachovej stejnou strukturu a organizaci jako v originálu
- Vyhýbej se frázím, které znějí jako od AI, dlouhým pomlčkám a příliš formálnímu jazyku
- Piš, jako bys byl rodilý mluvčí, který tvoří původní obsah
- Používej interpunkci odpovídající cílovému jazyku
Nepřekládej
- Názvy značek: "Foony", "CrazyGames" (jen tyhle dvě)
- Technické termíny, které jsou univerzální (např. "VIP", "XP")
- Emoji (ponechej přesně tak, jak jsou)
Markdown obsah k překladu:
Jak jsem vyřešil kaskádové změny hashů pomocí Import Maps
Čau! Tenhle problém jsem měl víc než 5 let, ale teprve teď jsem se rozhodl se do něj pustit, protože už to došlo do bodu, kdy ho prostě nešlo dál ignorovat. Když jsem změnil jediný znak v jednom souboru, polovina JavaScriptových souborů v mém buildu dostala nové hashované názvy, i když se jejich skutečný obsah vůbec nezměnil. To vedlo k úplně zbytečnému invalidování cache, skoro znemožnilo sledovat, co se mezi buildy vlastně změnilo, a co bylo nejhorší: rozbíjelo to moje buildy na Cloudflare Pages kvůli limitu počtu souborů.
Níž rozepíšu, v čem přesně byl problém, proč mi existující řešení nestačila a jak jsem si postavil vlastní Vite plugin, který to pomocí Import Maps jednou provždy vyřešil.
Problém: kaskádové změny hashů
Vite při produkčních buildech používá hashování založené na obsahu. Když buildneš aplikaci, každý JavaScriptový soubor dostane v názvu hash podle svého obsahu. Když se button.tsx zkompiluje na button-abc12345.js a obsah se změní, stane se z toho button-def45678.js. To je skvělé pro cache busting, uživatel vždy dostane nový soubor, když se změní.
Problém nastane ve chvíli, kdy soubor A importuje soubor B. Řekněme, že máš:
// main.js
import { Button } from "./button-abc12345.js";
Když se button.tsx změní, Vite vygeneruje button-def45678.js. Jenže teď se změní i main.js, protože v něm je řetězec "./button-abc12345.js", který je najednou špatně. Takže main.js dostane taky nový hash, i když se logika v main.js vůbec nezměnila.
Tohle se pak šíří celým tvým grafem závislostí. Změníš jednu pomocnou funkci a najednou má polovina JavaScriptových souborů nové hashe. V mém případě změna jediného znaku v useBackgroundMusic.ts způsobila, že se znovu přehashovalo přes 500 souborů.
Dopad v reálném světě byl docela velký. Bundlujeme 8 verzí assetů z minulých buildů, aby uživatelé na lehce zastaralých verzích klienta mohli pořád spustit tu svou verzi, i když nasadíme novou verzi na Cloudflare Pages. Jenže Cloudflare Pages má limit 20 000 souborů, na který jsme začali narážet kvůli naší dřívější změně v i18n, která brutálně zvýšila počet vytvářených souborů.
Vyřešením kaskádových hashů můžeme ukládat mnohem víc minulých buildů, aniž bychom na tyhle limity naráželi, protože většina souborů už se nemusí měnit. Taky to snižuje šanci, že uživatel na starším buildu skončí s chybou, protože je mnohem pravděpodobnější, že bude žádat o soubor, který se mezitím nezměnil a my ho pořád máme.
Proč ne [alternativní řešení]?
Když jsem se na to poprvé podíval, zvažoval jsem pár přístupů. Ale žádný mi úplně nesedl.
Post-build skripty
První nápad byl napsat post-build skript, který by normalizoval všechny import cesty, přepočítal hashe souborů a aktualizoval reference. Znělo to jednoduše: prostě regexem nahradit hashované názvy souborů za stabilní jména a pak znovu spočítat hashe.
Tenhle přístup jsem zavrhl kvůli "Heisenbugům" a riziku otrávené cache. I když uchováváme staré buildy na Cloudflare Pages, riziko nekonzistentní cache za to nestálo. Skript, který upravuje soubory až po buildu, může zavést nenápadné bugy, které se projeví jen v produkci, a ladit něco takového by byla čistá noční můra.
Vite manualChunks
Další možnost byla použít konfiguraci Vite manualChunks a oddělit stabilní kód (třeba node_modules) od nestabilního kódu (business logika). Myšlenka byla, že vendor kód se mění méně často, takže bude míň kaskádových změn.
To ale ve skutečnosti neřeší kořenový problém, jen ho trochu zmírňuje. Kaskádové hashe pořád dostáváš uvnitř chunků s business logikou. Chtěl jsem řešení, které se trefí do jádra problému, ne jen něco, co ho udělá o kousek míň nepříjemný.
Import Maps: moderní řešení
Import Maps jsou nativní funkce prohlížeče (s polyfillem pro starší prohlížeče), která odděluje modulové specifikátory od cest k souborům. Místo toho, aby soubor A importoval "./button-abc123.js", importuje "button". Prohlížeč pak pomocí import mapy přeloží "button" na skutečný hashovaný název souboru.
Tohle bylo přesně to, co jsem potřeboval. Obsah souboru A zůstává stejný (pořád importuje "button"), takže jeho hash se nemění. Nový hash dostane jen importní mapa a ten změněný soubor. Docela mě překvapilo, že na to ještě nikdo neudělal dobrý plugin!
Cesta k implementaci
Rozhodl jsem se postavit Vite plugin, který:
- Přepíše všechny relativní importy tak, aby používaly stabilní modulové specifikátory
- Vygeneruje importní mapu, která tyhle specifikátory namapuje na skutečné hashované názvy souborů
- Vloží importní mapu do HTML
Plugin je teď na GitHubu: @foony/vite-plugin-import-map
První pokus
Začal jsem s Vite pluginem, který používá hook generateBundle. V prvním pokusu jsem pomocí regexu hledal a nahrazoval import cesty. Kódovalo se to snadno a pro náš malý tým ve Foony to fungovalo, ale bylo to křehké a rozhodně by to nefungovalo jako plugin pro ostatní, kde by mohlo docházet k falešným shodám a neočekávaným úpravám.
Problémy s regexem byly jasné: co když nějaký string v kódu vypadá jako název souboru? Co dynamické importy? Co exporty? Pokud jsem chtěl plugin, který budou moct používat i ostatní, potřeboval jsem mnohem robustnější řešení.
Parsování AST
Potřeboval jsem JavaScriptový kód opravdu naparsovat a najít všechna importní prohlášení. První, po čem jsem sáhl, byl es-module-lexer, který je přímo navržený na parsování ES modulů. Bohužel během analýzy modulů ve Vite házel natívní paniky. Ani asm.js build nepomohl ty paniky zastavit.
Skončil jsem u Acorn, rychlého, lehkého parseru napsaného čistě v JavaScriptu. V kombinaci s acorn-walk na procházení AST mi dal přesně to, co jsem potřeboval, bez problémů s natívními závislostmi.
Klíčové problémy, které jsem musel vyřešit
Jak pokrýt všechny typy importů
Importy se v kódu objevují v různých podobách a v AST se s nimi zachází odlišně. Potřeboval jsem pokrýt:
- Statické importy:
import x from "./file.js" - Dynamické importy:
import("./file.js") - Pojmenované re-exporty:
export { x } from "./file.js"(tenhle jsem ze začátku přehlédl!) - Re-export všeho:
export * from "./file.js"
Re-export byl obzvlášť zrádný, protože jsem si ho všiml až ve chvíli, kdy jsem narazil na soubor, který se vůbec netransformoval. V kódu bylo export{PoolBalls,PoolCues,PoolTables}from"./Items-Bd_KmSuk.js" a můj plugin to úplně ignoroval, protože jsem hledal jen uzly ImportDeclaration a ImportExpression.
Takhle je teď všechny obsluhuju:
walk(ast, {
ImportDeclaration(node: any) {
// Statické importy: import x from "spec"
const specifier = node.source.value;
// ... logika transformace
},
ExportNamedDeclaration(node: any) {
// Pojmenované exporty se zdrojem: export { x, y } from "spec"
if (!node.source?.value) return;
// ... logika transformace
},
ExportAllDeclaration(node: any) {
// Re-export všeho: export * from "spec"
if (!node.source?.value) return;
// ... logika transformace
},
ImportExpression(node: any) {
// Dynamické importy: import("spec")
// ... logika transformace
},
});
Deterministické řešení konfliktů
Když má víc souborů stejný základní název (třeba několik index.tsx v různých složkách), potřebuju je od sebe odlišit. Nemůžu všem prostě říkat "index".
Moje řešení: když je konflikt, zhashuju původní cestu k souboru plus ten základní název. Například src/client/games/chess/index.tsx:index se zhashuje na index-abc123. Tím zajistím, že stejný soubor vždycky dostane stejný modulový specifikátor napříč buildy, i když přibudou nebo zmizí jiné soubory se stejným jménem.
Jako primární identifikátor používám chunk.facadeModuleId (vstupní bod), a když není k dispozici, spadnu zpátky k chunk.moduleIds[0]. Tím získám stabilní cestu ke zdrojáku pro deterministické hashování.
Řetězení source map
Když kód transformuju, přeruším řetěz source map. Existující source mapa mapuje původní TypeScript zdroj přes Babel a minifikaci až k současnému kódu. Moje transformace přidají další vrstvu, takže ten řetěz potřebuju zachovat.
Používám MagicString, abych sledoval svoje transformace a vygeneroval novou source mapu. Pak ji sloučím s tou existující tak, že zachovám původní pole sources a sourcesContent. Tím udržím celý řetěz: původní zdroj → (existující mapa) → transformovaný kód.
const existingMap = typeof chunk.map === 'string' ? JSON.parse(chunk.map) : chunk.map;
const newMap = magicString.generateMap({
source: fileName,
file: newFileName,
includeContent: true,
hires: true,
});
// Sloučení: použijeme mappingy z nové mapy, ale zachováme původní sources
chunk.map = {
...newMap,
sources: existingMap.sources || newMap.sources,
sourcesContent: existingMap.sourcesContent || newMap.sourcesContent,
file: newFileName,
};
Re-hashování transformovaného obsahu
Potřebuju, aby byl obsah souborů stabilní. Dělám to tak, že nejdřív transformuju importy (nahradím Vite hashované importy za svoje stabilní importy) a pak z výpočtu hashe vyhodím komentáře se source mapami (ty odkazují na staré názvy souborů).
Teprve potom spočítám nový hash a aktualizuju jak název souboru, tak záznam v importní mapě.
Finální implementace
Plugin používá čtyřprůchodovou strategii:
- Průchod s počítáním: zjistí konflikty v názvech tím, že spočítá, kolik souborů sdílí stejný základní název
- Mapovací průchod: vytvoří mapování chunků (hashovaný název souboru → modulový specifikátor) a počáteční importní mapu
- Transformační průchod: přepíše import cesty v kódu, znovu spočítá hashe a aktualizuje source mapy
- Přejmenovací průchod: aktualizuje názvy souborů v bundlu a finálně doplní importní mapu
Tady je jádro transformační logiky:
import {simple as walk} from 'acorn-walk';
// Naparsujeme kód a získáme AST
const ast = Parser.parse(chunk.code, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
});
const importsToTransform: Array<{start: number; end: number; replacement: string}> = [];
// Projdeme AST a najdeme všechny importy/exporty
walk(ast, {
ImportDeclaration(node: any) {
const specifier = node.source.value;
const filename = specifier.split('/').pop()!;
const moduleSpec = chunkMapping.get(filename);
if (moduleSpec) {
importsToTransform.push({
start: node.source.start + 1, // +1, abychom přeskočili počáteční uvozovku
end: node.source.end - 1, // -1, abychom přeskočili koncovou uvozovku
replacement: moduleSpec,
});
}
},
// ... obsluha dalších typů uzlů
});
// Aplikujeme transformace v opačném pořadí, aby seděly pozice
importsToTransform.sort((a, b) => b.start - a.start);
for (const transform of importsToTransform) {
magicString.overwrite(transform.start, transform.end, transform.replacement);
}
Pro vložení importní mapy do HTML používám Vite API na vkládání tagů místo regexu nad HTML:
transformIndexHtml() {
return {
tags: [
{
tag: 'script',
attrs: {type: 'importmap'},
children: JSON.stringify(importMap, null, 2),
injectTo: 'head-prepend',
},
],
};
}
To je mnohem spolehlivější než zkoušet regexem lovit HTML tagy.
V číslech
Ať máš představu, co tenhle plugin dělá:
- ~1 000+ JavaScriptových souborů zpracovaných na jeden build
- ~2–3 sekundy navíc k času buildu (přijatelný trade-off)
- ~99% snížení zbytečných změn hashů (většina souborů se teď mění jen při skutečné změně obsahu)
- ~340 řádků kódu pluginu (včetně komentářů a ošetření chyb)
Plugin zatím zvládá všechny edge casy, na které jsem narazil, a build proces je díky tomu mnohem předvídatelnější.
Co jsem si z toho odnesl
Proč je parsování AST nezbytné
Pouštět regex nad bundlovaným kódem je nebezpečné. Když nějaký string v kódu náhodou vypadá jako název souboru, regex ho přepíše. Parsování AST zajistí, že transformuješ jen skutečná import/export prohlášení.
Proč Acorn místo es-module-lexer
es-module-lexer je rychlejší a víc specializovaný, ale kvůli natívním panikám byl v kontextu Vite pluginu nepoužitelný. Acorn je čistě v JavaScriptu, takže se nemusím bát natívních závislostí. Do budoucna se na es-module-lexer asi znovu podívám jako na optimalizaci výkonu, ale teď mi Acorn bohatě stačí.
Proč Import Maps místo alternativ
Import Maps jsou webový standard s nativní podporou v prohlížečích. Jsou to ten "správný" způsob, jak tenhle problém řešit. Polyfill (es-module-shims) se elegantně postará o starší prohlížeče (např. Safari < 16.4) a celé řešení je čisté a dobře udržovatelné.
Závěr
Plugin s Import Maps mi úspěšně zabránil v kaskádových změnách hashů ve Vite buildech. Soubory teď dostanou nový hash jen tehdy, když se opravdu změní jejich obsah, ne když se změní jejich závislosti. Díky tomu jsou buildy předvídatelnější, nedochází ke zbytečnému invalidování cache a lépe se držíme pod limity počtu souborů na Cloudflare Pages.
Řešení je jednoduché, dobře udržovatelné a stojí na moderních webových standardech. Je to pěkný příklad toho, že někdy je to "správné" řešení zároveň i to nejjednodušší, jen je potřeba problém dostatečně dobře pochopit.
Plugin je open source a najdeš ho na GitHubu: @foony/vite-plugin-import-map. Nainstaluješ ho přes npm install @foony/vite-plugin-import-map a můžeš ho začít používat ve svých vlastních Vite projektech.
Do budoucna možná přidám optimalizaci pomocí es-module-lexer, až se vyřeší problémy s natívními panikami, nebo podporu pro ještě složitější scénáře importů. Ale v téhle chvíli plugin dělá přesně to, co od něj potřebuju.
A kdo ví, třeba jednou bude mít Vite něco takového zabudované přímo v sobě.