Astro integrates a compatibility build of Shiki, but up until at least version 5.1.1, it was actually compatible with the fully featured Shiki v2.0.0. That’s why I was able to highlight my inline code with getSingletonHighlightCore instead of the “compat” createHighlighter. I upgraded to 5.1.7 this morning and all my code blocks broke. I could’ve rolled it back, but I wanted to see what was up.
If some context is needed, this post explains a bit more in depth what I had to highlight inline code.
The Problem
There were two problems. The first is that Astro insisted on using its own version of Shiki types. So every Shiki import became plagued with errors that looked like this.
In this case, I was doing import { ShikiTransformer } from 'shiki';, but Astro really wanted me to use the identical version under @astrojs/markdown-remark that isn’t even exposed/exported.
The second problem was more serious. I got the following error and neither my inline nor blocked code work.
I tried filing a bug report with Astro, but their test environment on StackBlitz won’t show the same error message. It just failed silently. After some digging, I figured out that I could get block code to work, but not inline code, by adding this line.
10//https://astro.build/config11exportdefaultdefineConfig({12site:SITE_URL,13integrations:[sitemap(),svelte()],14markdown:{15shikiConfig:{languages, themes, transformers, etc.
I eventually found that if I commented out specifically the lines that called the highlighter outside of Astro integration and did e.g. codeToHast with it, the code would run (without highlighting my inline code of course). To fix it, it was something like this.
104consthighlightCode=()=>{constcachedHighlighter=getSingletonHighlighterCore({langs:[resolveLanguage('plaintext')],themes:Object.values(shikiThemes).map(resolveTheme)});105constcachedHighlighter=createShikiHighlighter({Type 'string' is not assignable to type 'LanguageRegistration'.ts(2322)106langs:['plaintext'],107themes:shikiThemes108});109returnasync(tree:Root)=>{
Uh, what? Astro’s “convenience” function wraps around Shiki’s createHighlighter and doesn’t actually return a highlighter, making it impossible to add languages later. Not to mention it wrapping the parameters in unnecessary types.
151consthighlighter=awaitcachedHighlighter;152for(constinstanceininlineInstances){153constresolvedLanguage=resolveLanguage(instance.language);154if(resolvedLanguage){Shiki's highlighters have the same methods, so we didn't need to change this after all.155awaithighlighter.loadLanguage(156resolveLanguage(resolvedLanguage));
Meanwhile, Astro is still complaining about the types, so I decided to just swap out its Shiki integration altogether, for both inline and code blocks.
I already had the code to find inline code from HAST code (a tree representation of a document). All I need to do then is to add a branch to detect code blocks.
24constisInlineCode=(25element:Element,26parent:Element|Root|undefined27):boolean=>{The default render of inline code is <code>Text</code>.28return(29element.tagName==='code'&&30parent.type==='element'&&31parent.tagName!=='pre'&&32element.children.length===1&&33element.children[0].type==='text'34);35};36constisBlockCode=(37element:Element,38parent:Element|Root|undefined39):boolean=>{The default render of a code block is <pre><code class=language-[language]>Text</code></pre>. The <pre> block distinguishes between inline and block code.40return(41element.tagName==='pre'&&42element.children.length===1&&43element.children[0].type==='element'&&44element.children[0].tagName==='code'&&45element.children[0].children[0].type==='text'46);47};
skip to line 100
100interfaceVisitCallback{101node:Element;102index?:number;103parent?:Element|Root;104}105interfaceCodeInstance{106node:Element;107code:string;108language:string;109meta?:string110}Inline code are expected to be in the format of Text{:Language}.111constregexp=/^(.+){:(\w+)}$/;112consthighlightCode=()=>{Create cached highlighter.
skip to line 117
117returnasync(tree:Root)=>{118constinlineInstances:CodeInstance[]=[];119constblockInstances:CodeInstance[]=[];Visit each element in the document in order.120visit(tree,'element',({node,index,parent}:VisitCallback)=>{121if(isInlineCode(node,parent)){122consttextNode=node.children[0]asText;123constmatch=textNode.value.match(regexp);124if(match){125const[_,code,language]=match;126inlineInstances.push({node,code,language});127}128else{129inlineInstances.push({node,code,'plaintext'});130}131}132elseif(isBlockCode(node,parent)){133constcodeElement=node.children[0]asElement;134letmeta=codeElement.data?.meta;135if(!meta)meta=undefined;136consttextNode=codeElement.children[0]asText;Expect <code class=language-[language]>.137constlanguage=(138codeElement.properties['className']asstring139)[0].split('-')[1];140blockInstances.push({node,code,language,meta});141}142});
skip to line 151
151consthighlighter=awaitcachedHighlighter;152for(constinstanceofinlineInstances){Load languages if valid.
skip to line 163
163constroot=highlighter.codeToHast(instance.code,{164lang:instance.language,165themes:shikiThemes,166defaultColor:false,167transformers:[{168//Dosomestyling169}]170});171constpre=root.children[0]asElement;172constcode=pre.children[0]asElement;173instance.node.properties={...pre.properties};174instance.node.children=code.children;175}176for(constinstanceofblockInstances){Load languages if valid.
skip to line 185
185constroot=highlighter.codeToHast(instance.code.trimEnd(),{186lang:instance.language,187themes:shikiThemes,188defaultColor:false,189transformers:[190{191pre(pre){192pre.properties['data-language']=instance.language;193}194},Import all the code block transformers that used to run on shikiConfig.195transformer(instance.meta)196]197});198constfigure=root.children[0]asElement;199instance.node={...figure,properties:{...figure.properties}};200}201}202};203exportdefaulthighlightCode;
Finally, somewhere in the beginning of the transformers function, we need to initialize the meta data as it worked in Shiki.
typescript
1consttransformer=(metadata:string):ShikiTransformer=>{2return{3preprocess(code,options){4options.meta=options.meta||{};5options.meta.__raw=metadata;... the rest of the transform
skip to line 128
128}129}130};131exportdefaulttransformer;
What’s next?
I think there’s no disadvantage to reducing dependencies. Before with the Astro integrations, I’m at the mercy of both Astro and Shiki. If either of these tools update, I may have to spend a day figuring it out. Now besides issues with Vite, I’m at the mercy of Shiki only. I can breathe easy updating Astro knowing that it likely won’t mess with my blog posts, since the syntax highlighting is the most complicated piece of machinery in there. That trepidation shouldn’t hold me back from potential bug fixes and new features.
One question remains: Is it still possible to not use the shiki/compatcreateHighlighter? I’ll be on the lookout for that, although this problem might be too involved for me right now. It seems to have something to do with Vite’s interaction with web assembly, and that’s not something I can diagnose easily. I’ll most likely set this aside at some point and move on to the next project.