記事·  

Shiki v1.0 の進化

Shiki v1.0 には多くの改善と新機能が搭載されています。Nuxt がどのように Shiki の進化を推進しているかをご覧ください!
Anthony Fu

Anthony Fu

@antfu

Shikiは、TextMate 文法とテーマを使用するシンタックスハイライターであり、VS Code と同じエンジンを搭載しています。これにより、コードスニペットに対して最も正確で美しいシンタックスハイライトが提供されます。これはPine Wuが 2018 年に VS Code チームの一員であったときに作成されました。Onigurumaを使用してシンタックスハイライトを行う実験として始まりました。

既存のシンタックスハイライターとは異なり、PrismGlobalComponentsHighlight.jsブラウザで実行するように設計されたShikiは、**事前にハイライトする**という異なるアプローチを取りました。ハイライトされたHTMLをクライアントに送信し、**JavaScriptなし**で正確で美しいシンタックスハイライトを生成します。これはすぐに人気を博し、特に静的サイトジェネレーターやドキュメントサイトで非常に人気のある選択肢となりました。

Shiki は素晴らしいですが、Node.js で実行するように設計されたライブラリです。つまり、静的コードのハイライトに限定され、動的コードには問題があります。なぜなら、Shiki はブラウザで動作しないからです。さらに、Shiki は Oniguruma の WASM バイナリや、JSON 形式の重い文法ファイルとテーマファイルに依存しています。これらのファイルをロードするために Node.js のファイルシステムとパス解決を使用しますが、これはブラウザではアクセスできません。

この状況を改善するために、私はこの RFC を開始し、その後この PRとして取り込まれ、Shiki v0.9 でリリースされました。これは、環境に基づいてフェッチまたはファイルシステムを使用するようにファイルロードレイヤーを抽象化しましたが、グラムとテーマファイルをバンドルまたは CDN に手動で提供し、`setCDN` メソッドを呼び出して Shiki にこれらのファイルのロード場所を伝える必要があるため、使用はかなり複雑でした。

この解決策は完璧ではありませんでしたが、少なくとも Shiki をブラウザで実行して動的コンテンツをハイライトすることを可能にしました。それ以来、このアプローチを使用してきました。この記事の物語が始まるまでは。

始まり

Nuxt は、ウェブをエッジに推進し、低レイテンシと優れたパフォーマンスでウェブのアクセシビリティを向上させることに多大な努力を払っています。CDN サーバーと同様に、CloudFlare Workersのようなエッジホスティングサービスは世界中にデプロイされています。ユーザーは、数千マイル離れたオリジンサーバーへのラウンドトリップなしに、最寄りのエッジサーバーからコンテンツを取得します。これにより提供される素晴らしい利点には、いくつかのトレードオフも伴います。例えば、エッジサーバーは制限されたランタイム環境を使用します。CloudFlare Workers はファイルシステムアクセスもサポートせず、通常、リクエスト間で状態を保持しません。Shiki の主なオーバーヘッドは文法とテーマを事前にロードすることですが、それはエッジ環境ではうまく機能しません。

すべてはセバスチャンと私との間のチャットから始まりました。私たちはNuxt ContentShiki を使用してコードブロックをハイライトし、エッジで動作させることを試みていました。

私は、shiki-esプーヤ・パーサによる Shiki の ESM ビルド)をローカルでパッチ適用することから実験を始めました。これは、文法とテーマのファイルをECMAScript Module (ESM)に変換して、ビルドツールが理解してバンドルできるようにするためでした。これは、ファイルシステムやネットワークリクエストを使用せずに CloudFlare Workers が利用できるコードバンドルを作成するために行われました。

変更前 - ファイルシステムから JSON アセットを読み込む
import fs from 'fs/promises'

const cssGrammar = JSON.parse(await fs.readFile('../langs/css.json', 'utf-8'))
変更後 - ESM インポートを使用
const cssGrammar = await import('../langs/css.mjs').then(m => m.default)

JSON ファイルをインラインリテラルとして ESM にラップして、import()を動的にインポートできるようにする必要があります。違いは、`import()` はどこでも機能する標準的な JavaScript 機能であるのに対し、`fs.readFile` は Node.js 固有の API であり、Node.js でのみ機能することです。`import()` を静的に持つことで、RollupGlobalComponentswebpackのようなバンドラーもモジュール関係グラフを構築し.

その後、エッジランタイムで動作させるには、それ以上のことが必要だと気づきました。バンドラーはビルド時にインポートが解決可能であることを期待しているため(つまり、すべての言語とテーマをサポートするためには)、コードベース内のすべての文法ファイルとテーマファイルにすべてのインポートステートメントをリストアップする必要があります。これにより、実際に使用しないであろう大量の文法とテーマを含む巨大なバンドルサイズになってしまいます。この問題は、バンドルサイズがパフォーマンスに不可欠なエッジ環境では特に重要です。

そこで、より良い中間点を見つける必要がありました。

フォーク - Shikiji

これは Shiki の動作を根本的に変える可能性があることを知り、既存の Shiki ユーザーを我々の実験で壊すリスクを冒したくなかったため、私は Shiki のフォークであるShikijiを開始しました。以前の API 設計の決定を念頭に置きながら、コードをゼロから書き直しました。目標は、Shiki をランタイムに依存せず、パフォーマンスが高く効率的にすることです。これはUnJS.

の哲学と同様です。これを実現するためには、Shikiji を完全に ESM フレンドリーで、純粋で、ツリーシェイク可能にする必要があります。これは、GlobalComponentsvscode-onigurumavscode-textmateのような Shiki の依存関係にも及びます。これらはCommon JS (CJS)形式で提供されています。`vscode-oniguruma` には、emscriptenによって生成された WASM バインディングも含まれており、CloudFlare Workers がリクエストを完了できない原因となる宙ぶらりんのプロミスが含まれています。最終的に、WASM バイナリをbase64 文字列に埋め込み、ES モジュールとして出荷し、宙ぶらりんのプロミスを避けるために WASM バインディングを手動で書き換え、vscode-textmate をベンダーし

てソースコードからコンパイルし、効率的な ESM 出力を生成しました。その結果は非常に有望でした。Shikiji はあらゆるランタイム環境で動作するようになり、CDN からインポートしてブラウザで実行する

ことも可能になりました。また、Shiki の API と内部アーキテクチャを改善する機会も捉えました。単純な文字列連結からhastを使用するように切り替え、HTML 出力を生成するための抽象構文木(AST)を作成しました。これにより、Transformers API

を公開して、ユーザーが中間 HAST を変更し、以前は非常に困難だった多くのクールな統合を実現できる可能性が開かれました。ダーク/ライトモードのサポートは頻繁にリクエストされる機能でした。Shiki が採用する静的なアプローチのため、レンダリング時にテーマをその場で変更することはできません。これまでの解決策は、ハイライトされた HTML を2回生成し、ユーザーの好みに応じて表示を切り替えることでした。これはペイロードを重複させるため効率的ではなく、あるいはCSS 変数テーマを使用していましたが、Shiki の得意とするきめ細やかなハイライトを失っていました。Shikiji の新しいアーキテクチャにより、私は問題を再考し、共通のトークンを分解し、複数のテーマをインライン CSS 変数としてマージするというアイデアを考案しました。これにより、Shiki の哲学に沿った効率的な出力を提供できます。詳細については、.

Shiki のドキュメントをご覧ください。移行を容易にするため、Shikiji の新しい基盤を使用し、後方互換性のある API を提供する

shikiji-compat 互換性レイヤーも作成しました。ShikijiをCloudflare Workersで動作させるには、インラインバイナリデータからのWASMインスタンスの初期化をサポートしていないという最後の課題がありました。代わりに、セキュリティ上の理由から静的な`.wasm`アセットのインポートが必要になります。これは、「All-in-ESM」アプローチがCloudFlareではうまく機能しないことを意味します。これでは、ユーザーが異なるWASMソースを提供するために余分な作業が必要となり、私たちが意図したよりも体験が困難になります。この時、プーヤ・パーサunjs/unwasmというユニバーサルレイヤーを作成し、今後のWebAssembly/ESモジュール統合提案をサポートするようにしました。これはNitroに統合され、WASMターゲットを自動化するようになりました。私たちは、`unwasm`がWASMを扱う開発者にとってより良い体験を提供できることを願っています。

全体として、Shikiji の書き換えはうまくいきました。Nuxt Content, VitePressGlobalComponentsAstroに移行されました。受け取ったフィードバックも非常に肯定的です。

統合

私は Shiki のチームメンバーであり、時折リリースを手伝ってきました。Pineは Shiki のリーダーですが、他のことで忙しく、Shiki のイテレーションは遅れていました。Shikiji の実験中に、私はいくつかの改善を提案し、Shiki が現代的な構造を獲得するのに役立つ可能性がありました。一般的に誰もがその方向性に同意しましたが、かなりの作業量があり、誰もそれに取り掛かりませんでした。

Shikiji を使って抱えていた問題を解決できたことは喜ばしいことでしたが、Shiki の2つの異なるバージョンによってコミュニティが分裂するのを見ることは望んでいませんでした。Pine との電話の後、2つのプロジェクトを1つに統合するという合意に至りました。

feat!: Shikiji を Shiki v1.0 に統合 #557

Shikiji での私たちの作業が Shiki に統合されたことを大変嬉しく思います。これは私たち自身だけでなく、コミュニティ全体に利益をもたらします。この統合により、Shiki が長年抱えていた**未解決の課題の約95%が解決されました**。

Shiki には新しいドキュメントサイトもできました。ブラウザでShikiを試すこともできます(アグノスティックなアプローチのおかげです!)。多くのフレームワークがShikiと統合されており、すでにどこかで使っているかもしれません!

Twoslash

Twoslashは、TypeScript Language Servicesから型情報を取得し、コードスニペットに生成するための統合ツールです。基本的に、静的なコードスニペットに VS Code エディターと同様のホバー型情報を持たせます。これはOrta TheroxによってTypeScript ドキュメントサイトのために作成されました。元のソースコードはこちらで確認できます。Orta はShiki v0.x バージョン用の Twoslash 統合も作成しました。当時、Shiki には適切なプラグインシステムがありませんでした。そのため、`shiki-twoslash` は Shiki のラッパーとして構築する必要があり、既存の Shiki 統合が Twoslash と直接連携しないため、セットアップが少し困難でした。

Shikiji の書き換え中に、Twoslash の統合も見直す機会を得ました。これは、ドッグフーディングと拡張性の検証にもなりました。新しい HAST 内部構造により、Twoslash をトランスフォーマープラグインとして統合できるようになり、Shiki が動作するあらゆる場所で、他のトランスフォーマーと組み合わせ可能な方法で動作するようになりました。

これにより、私たちは Twoslash をnuxt.com (あなたがご覧になっているこのウェブサイト) で動作させられるかもしれないと考え始めました。nuxt.com はNuxt Contentを内部で使用しており、VitePress のような他のドキュメントツールとは異なり、Nuxt Content が提供する利点の1つは、動的コンテンツを処理し、エッジで実行できることです。Twoslash は TypeScript と依存関係にある巨大な型モジュールグラフに依存しているため、これらすべてをエッジやブラウザに提供することは理想的ではありません。難しいように聞こえますが、挑戦を受け入れました!

最初に、CDN からオンデマンドで型をフェッチするというアイデアを思いつきました。これは、TypeScript Playgroundで見るAuto-Type-Acquisition技術を使用します。私たちはtwoslash-cdnを作成し、Twoslash があらゆるランタイムで動作するようにしました。しかし、これはまだ最適な解決策ではないようです。なぜなら、エッジで実行する目的を損なう可能性のある多くのネットワークリクエストを依然として行う必要があるからです。

基盤となるツール (例えば、@nuxtjs/mdcNuxt Content で使用されている Markdown コンパイラ) で数回のイテレーションを行った後、私たちはハイブリッドアプローチを取り、nuxt-content-twoslashを作成しました。これは、ビルド時に Twoslash を実行し、エッジレンダリングのために結果をキャッシュします。この方法で、最終的なバンドルに余分な依存関係を同梱することなく、ウェブサイト上にリッチでインタラクティブなコードスニペットを持つことができました。

<script setup>
// Try hover on identifiers below to see the types
const count = useState('counter', () => 0)
const double = computed(() => count.value * 2)
</script>

<template>
  <button>Count is: {{ count }}</button>
  <div>Double is: {{ double }}</div>
</template>

その間、私たちはOrtaと一緒にTwoslashをリファクタリングし、より効率的で現代的な構造にする機会も捉えました。これにより、twoslash-vueも実現し、Vue SFCサポートが提供され、上記で試しているように動作します。これはVolar.jsGlobalComponentsvuejs/language-toolsによって提供されています。Volarがフレームワークに依存せず、フレームワークが連携して動作するようになるにつれて、将来的にはAstroやSvelteコンポーネントファイルのようなより多くの構文にこのような統合が拡大することを楽しみにしています。

統合

ご自身のウェブサイトで Shiki を試してみたい場合は、私たちが作成したいくつかの統合を見つけることができます。

詳細な統合については、Shiki のドキュメント

をご覧ください。

結論Shiki, Nuxt のミッションは、開発者のためのより良いフレームワークを作成することだけでなく、フロントエンドと Web エコシステム全体をより良い場所にすることです。私たちは限界を押し広げ続け、最新の Web 標準とベストプラクティスを支持しています。新しい, Twoslashunwasm

と、Nuxt と Web をより良くするために作成した他の多くのツールをお楽しみいただければ幸いです。