Repository navigation
feat: preloadLanguages accepts dynamic grammar imports - #193
Conversation
Full/web bundles now take shiki LanguageInput entries (getters, promises, module objects) in preloadLanguages, so custom grammars stay code-split instead of shipping in the main bundle. Dynamic entries bypass name matching, dedupe by reference, and resolve inside shiki during highlighter setup. Additive only.
🦋 Changeset detectedLatest commit: da6628c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| ``` | ||
|
|
||
| > [!IMPORTANT] | ||
| > Define dynamic imports at module scope. A fresh `import()` promise or arrow function on every render re-triggers highlighting. |
There was a problem hiding this comment.
Docs accuracy (low): the "fresh promise re-triggers highlighting" claim doesn't hold for bare promises — only for functions.
useShikiHighlighter stabilizes options with useStableValue, which falls back to dequal. dequal compares two objects with the same constructor by their own enumerable keys — a Promise has none, so any two Promise instances compare equal. Functions, by contrast, hit dequal's strict-equality fallback and always compare unequal. Concretely:
- A fresh arrow/getter per render → re-triggers highlighting (as documented ✅)
- A fresh bare
import()promise per render → treated as unchanged, no re-trigger (contradicts this callout, though the behavior is actually benign) - The sharper edge: intentionally replacing a bare-promise entry with a different promise (different grammar) is silently ignored —
stableOptskeeps the first promise and the new grammar never loads. Same applies to the{ default: grammar }module-object form (deep-equal → stable).
Since import() promises are the form this doc snippet's naming evokes, consider steering users to the getter form (() => import(...)) as the canonical dynamic entry — which the examples already do — and softening this callout to something like: "Define getters at module scope; a fresh arrow per render re-triggers highlighting. Bare promises are compared by type, so swapping one at runtime won't be picked up." Alternatively, languageKey could be taught to treat promises as unstable, but a docs fix seems proportionate to the edge.
There was a problem hiding this comment.
Verified: dequal compares two distinct Promise instances as equal (same constructor, no own enumerable keys), while fresh arrows always compare unequal. Confirmed empirically against the shipped dequal:
dequal(Promise.resolve(1), Promise.resolve(2))→truedequal(() => 1, () => 1)→falsedequal({ default: grammarA }, { default: grammarA })→true
So the callout's promise half was wrong, and the sharper edge (swapping a bare-promise entry at runtime being silently ignored) is real. Agreed a docs fix is proportionate — teaching languageKey to treat promises as unstable would defeat useStableValue for everyone using them correctly at module scope.
Fixed in 906ba98: the callout now steers to the getter form and notes that bare promises are compared by type, so runtime swaps won't be picked up.
|
Review: preloadLanguages accepts dynamic grammar imports Verdict: clean, well-typed, well-tested, and properly documented. Two low-severity notes left inline; neither blocks merge. Checklist
Code quality The implementation is nicely factored. Findings (2, both low severity, inline)
Non-issues verified
🤖 Reviewed by Fable 5 |
Summary
preloadLanguagesnow accepts shiki'sLanguageInputentries (getters, promises, module objects) on the full and web bundles, so custom grammars can stay code-split instead of shipping in the main bundle:Shiki's
getSingletonHighlighteralready resolvesLanguageInputnatively; this unblocks react-shiki's resolution layer:PreloadLanguage = Language | LanguageInputtype, exported from all three entrieso:undefined::undefinedkey), and pass the factory's loadability filterisDynamicLanguageguard narrows toExclude<LanguageInput, LanguageRegistration>so the registration branch stays fully typed with no castsAdditive only
language.test.tstests untouched and passingpreloadLanguagesremains a no-op with a custom highlighterTesting
Docs
README Preloading Languages section gains the dynamic import example with an IMPORTANT callout: define entries at module scope, since a fresh promise or arrow per render re-triggers highlighting.