Nuxt Nation カンファレンスが始まります。11 月 12 日から 13 日に参加してください。
記事·  

シキ v1.0 の進化

シキ v1.0 には多くの改善と機能が追加されました。シキの進化を Nuxt がどのように推進しているかをご覧ください。

シキTextMate の文法とテーマ を使用する構文強調表示機能であり、VS コードと同じエンジンを搭載しています。コードスニペット用の最も正確で美しい構文強調表示を提供します。2018 年に Pine Wu が VS コードチームの一員だったときに作成しました。構文強調表示に Oniguruma を使用する実験として始まりました。

PrismHighlight.js などの既存のシンタックスハイライターとは異なり、シキはブラウザで実行されるように設計されたものとは異なるアプローチを採り、事前に強調表示しています。強調表示された HTML をクライアントに送信し、ゼロ JavaScript で正確で美しいシンタックスハイライトを生成します。すぐに脚光を浴び、特にスタティックサイトジェネレーターやドキュメンテーションサイトで非常に人気のある選択になりました。

シキは素晴らしいものですが、Node.js で実行されるように設計されたライブラリです。つまり、静的なコードのみを強調表示する必要がある場合に限定され、シキはブラウザで動作しないため、動的なコードでは問題が発生します。さらに、シキは Oniguruma の WASM バイナリと、JSON 内の多数の重厚な文法ファイルやテーマファイルに依存しています。Node.js のファイルシステムとパス解決を使用してこれらのファイルをロードしていますが、これらはブラウザではアクセスできません。

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

ソリューションは完璧ではありませんが、少なくとも動的コンテンツを強調表示するためにブラウザでシキを実行できるようになりました。それ以来、このストーリーが始まるまで、そのアプローチを使用してきました。

始まり

Nuxt は ウェブを端に押し出す ことに多くの労力を費やし、ウェブを遅延を削減し、パフォーマンスを向上させてさらにアクセスしやすくしています。CDN サーバーと同様に、CloudFlare Workers などのエッジホスティングサービスは世界中に展開されています。ユーザーは、何千マイルも離れた可能性のある元のサーバーへのラウンドトリップなしで、最も近いエッジサーバーからコンテンツを取得します。それがもたらす素晴らしい利点とともに、いくつかのトレードオフが伴います。たとえば、エッジサーバーは制限されたランタイム環境を使用します。CloudFlare Workers もファイルシステムアクセスをサポートせず、通常はリクエスト間で状態を保持しません。シキの主なオーバーヘッドは文法とテーマを事前にロードすることですが、これはエッジ環境では適切に機能しません。

話は セバスチャン とのチャットから始まりました。作業に Shiki を使用してコードブロックをハイライトする Nuxt Content をエッジで機能するようにしていました。

Chat History Between Sébastien and Anthony

実験は shiki-esPooya Parsa による Shiki の ESM ビルド)をローカルでパッチを当てることから始めました。その目的は、文法ファイルとテーマファイルを ECMAScript モジュール (ESM) に変換し、ビルドツールが認識してバンドルできるようにすることです。これは CloudFlare Workers のコードバンドルを作成するために、ファイルシステムの使用やネットワークリクエストを行わずに行いました。

前: ファイルシステムから JSON アセットを読み取り
import fs from 'fs/promises'

const cssGrammar = JSON.parse(await fs.readFile('../langs/css.json', 'utf-8'))
後: ESM import を使用
const cssGrammar = await import('../langs/css.mjs').then(m => m.default)

JSON ファイルをインラインリテラルとして ESM にラップし、import() を使用して動的にインポートできるようにする必要があります。違いは import() がどこでも利用できる標準の JavaScript 機能であるのに対し、fs.readFile は Node.js 特有の API で Node.js でしか動作しないことです。また、import() を静的に含めることで、Rollupwebpack といったバンドラーがモジュール関連のグラフを作成し、バンドルされたコードをチャンクとして生成できるようになります。

その後、エッジランタイムで動作させるにはそれ以上のものが必要なことに気がつきました。バンドラーは import 時にビルド時点で解決可能であることを前提にしています(つまりすべての言語とテーマをサポートするには)コードベース内のすべての文法ファイルとテーマファイル内のすべてのインポートステートメントをリストする必要があります。これにより、実際には使用しない可能性の高い無数の文法とテーマを含む巨大なバンドルサイズになってしまいます。バンドルサイズがパフォーマンスに影響を与えるエッジ環境では、特にこの問題が重要です。

そこで、よりバランスの良い解決策を見つける必要がありました。

フォーク版: Shikiji

これにより Shiki の仕組みが根本的に変わる可能性があることがわかり、また実験で既存の Shiki ユーザーを不安にされるリスクを負いたくなかったので、Shikiji という名前の Shiki のフォーク版を作成しました。これまでの API デザインの決定事項を念頭に置きながらコードをゼロから書き直しました。目的は UnJS の理念と同様に、Shiki をランタイムに依存しない、高性能で効率的なものにすることです。

実現するには、Shikijiを完全にESMフレンドリーで、純粋かつツリーシャカブルにする必要があります。それはvscode-onigurumavscode-textmateといった、Shikiの依存関係すべてに及びます。それらはCommon JS (CJS)フォーマットで提供されています。vscode-onigurumaにはemscriptenによって生成されたWASMバインディングも含まれており、未解決のプロミスが含まれており、CloudFlare Workersによるリクエストの完了を妨げます。WASMバイナリをbase64文字列に埋め込み、ESモジュールとして配布し、WASMバインディングを手動で書き直して未解決のプロミスを回避し、vscode-textmateベンダー化してソースコードからコンパイルし、効率的なESM出力を生成します。

最終結果は非常に有望です。私たちはどんなランタイム環境でも、CDNからインポートして単一コード行でブラウザで実行することさえ可能なShikijiを利用できるようになりました。

また、ShikiのAPIと内部アーキテクチャを改善する機会を得ました。単純な文字列連結からhastを使用し、HTML出力を生成する抽象構文木(AST)を作成することに切り替えました。これにより、ユーザーが中間的なHASTを変更して、以前は実現が非常に困難だった多くの素晴らしい統合を行えるトランスフォーマーAPIを公開できる可能性が出てきました。

ダーク/ライトモードのサポートは頻繁に要求される機能でした。Shikiが採用する静的なアプローチのため、レンダリング中にテーマをその場で変更することはできません。これまでの解決策は、ハイライトされたHTMLを2度生成し、ユーザーの好みによってそれらの表示を切り替えることでした。その方法はペイロードが重複してしまうため効率が悪いか、Shikiの得意とする粒度の細かいハイライトが失われてしまうCSS変数のテーマを使用していました。Shikijiが採用した新しいアーキテクチャでは、私は一歩下がって問題を再考し、共通トークンを分解し、複数のテーマをインラインCSS変数としてマージするアイデアを思いつきました。これにより、Shikiの哲学と一致しつつ、効率的な出力がもたらされます。これの詳細についてはShikiのドキュメントで確認できます。

移行を容易にするため、Shikijiの新しい基盤を使用して後方互換APIを提供するshikiji-compat互換レイヤーも作成しました。

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

全体として、Shikijiの書き換えはうまくいきました。Nuxt ContentVitePressAstroはこれにマイグレーションされました。私たちが受け取ったフィードバックもとても前向きです。

マージバック

私はShikiのチームメンバーで、時々リリースを手伝ってきました。PineはShikiのリーダーですが、別のことに忙しく、Shikiのイテレーションが遅れました。Shikijiでの実験中、Shikiが最新の構造を獲得するのに役立ついくつかの改善点を提案しました。一般的には誰もがこの方向性に同意していましたが、やるべきことはたくさんあるだろうし、誰もそれに取り掛かりませんでした。

Shikijiを使用して私たちが抱えていた問題を解決できたことを嬉しく思いましたが、Shikiの2つの異なるバージョンによってコミュニティが分裂するのを見ることは望んでいませんでした。パインとの電話の後、2つのプロジェクトを1つにマージするというコンセンサスを得ました。

feat!: v1.0向けにShikijiをShikiにマージ#557

私たち自身のだけでなくコミュニティ全体にも利益をもたらす、Shikijiでの作業がShikiにマージされたのを見て、本当に嬉しく思います。このマージにより、Shikiで長年抱えていた未解決の約95%が解決されます。

Shikiji Merged Back to Shiki

Shikiでも真新しいドキュメントサイトができました。ブラウザでそのまま利用することもできます(汎用的なアプローチのおかげです)。多くのフレームワークが現在Shikiと統合されています。もしかすると、すでにどこかで使用しているかもしれません。

Twoslash

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

Shikijiを書き換えているときに、Twoslash統合を修正する機会も得ました。これはドッグフーディングをして拡張性を検証する方法でもありました。新しいHAST内部を使用して、Twoslashをトランスフォーマープラグインとして統合できます。これにより、Shikiが機能する場所ならどこでも機能し、他のトランスフォーマーで使用する際の構成可能な方法にもなります。

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

最初にCDNからタイプをオンデマンドフェッチする方法を考えつきました。 TypeScript playgroundで見られるAuto-Type-Acquisitionテクニックを使用しています。 twoslash-cdnを作成して、Twoslashを任意のランタイムで実行できるようにしました。しかし、それでも最適なソリューションではないと思われます。ネットワークリクエストを多数行う必要があり、エッジで実行する目的が果たされない可能性があるからです。

基盤となるツールに関するいくつかのイテレーションの後(例: @nuxtjs/mdc、Nuxt Contentで使用されるマークダウンコンパイラ)、ハイブリッドアプローチを採用し、ビルド時にTwoslashを実行し、エッジレンダリング用に結果をキャッシュするnuxt-content-twoslashを作成しました。これにより、最終的なバンドルへの追加の依存関係の配布を回避しながら、Webサイトにリッチなインタラクティブコードスニペットを表示できます

<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をリファクタリングして、より効率的でモダンな構造にしました。これにより、上記でプレイしているVue SFCのサポートを提供するtwoslash-vueも作成できます。 Volar.jsvuejs/language-toolsによって強化されています。Volarがフレームワークに依存せず、フレームワーク同士が連携するようになるにつれて、AstroやSvelteコンポーネントファイルなどの構文へのこのような統合が、将来的に拡大することを期待しています。

統合

独自のWebサイトでShikiを試してみたい場合は、Shikiで行ったいくつかの統合があります

Shikiのドキュメントでさらに多くの統合を確認してください

結論

Nuxtの使命は、開発者向けのより良いフレームワークを作成するだけでなく、フロントエンドやWebエコシステム全体をより良い場所にすることです。 私たちは境界線を押し広げ、最新のWeb標準とベストプラクティスを支持し続けています。新しいShikiunwasmTwoslash、そしてNuxtとWebを改善するプロセスで行った他の多くのツールを楽しんでいただければ幸いです。

← ブログに戻る