zfb
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

ブラウザでの Markdown プレビュー

@takazudo/zfb-md-wasm パッケージを使い、MDX を実行可能な ES モジュールへコンパイルしたり、Markdown を HTML へレンダリングしたりを、zfb のビルド時パイプラインと出力パリティを保ちながら、すべてブラウザ内で行います。

このページで扱う内容

@takazudo/zfb-md-wasm、zfb の md/mdx → JS/HTML パイプラインの ブラウザ側での動的変換向け WebAssembly ビルド。完全なルート API(compile()renderHtml())、parseToAst()による生の mdast ツリーへのパース(および検証付きの toMdastRoot() アダプタ)、直接的な セマンティックな highlightCode()、ブラウザでコンパイル済みモジュールを評価する方法、 Vite でのパッケージの使い方、テストとツールでの Node 利用、そしてパリティ保証の明示的な 制限を扱います。

これは何か、なぜ存在するのか

@takazudo/zfb-md-wasm は、zfb 自身の markdown/MDX → JS 変換パイプラインを WebAssembly にコンパイルし、あなたのマシンでのビルド時だけでなく、ブラウザのタブ内でも実行できる ようにします。

代表的なユースケースは CMS のライブプレビュー です: 編集者が CMS に Markdown や MDX を 入力するとき、プレビューペインは zfb build が最終的に生成するものを — 近似ではなく — そのまま表示すべきです。これを @mdx-js/mdx をブラウザで実行して 行うと、独自のパース、プラグイン、JSX 出力の挙動を持つ 別の パイプラインを使うことに なり、そのプレビューは zfb の実際のビルド出力からずれる可能性があります。zfb-md-wasm は、 zfb 自身がビルド時に使うのと 同じ Rust パイプライン をコンパイルすることでそのずれを 避けるため、プレビューは実際に出荷されるものに忠実であり続けます。パリティこそが、単に @mdx-js/mdx に手を伸ばすのではなくこのパッケージが存在する、そのすべての理由です。

Node ≥ 20 でもこのパッケージをロードできます — パッケージ自身のテストスイートがそう しています — が、ターゲットはブラウザです。zfb バイナリを直接実行するシェルアクセスを 持つサーバーは、このパッケージをロードするのではなく、引き続きそうすべきです。

Note

pnpm add @takazudo/zfb-md-wasm(またはお好みのパッケージマネージャ)でインストール します。

最小のエントリを選ぶ

ルート(@takazudo/zfb-md-wasm)は完全な後方互換の面で、compile を export する唯一の エントリです。既存のルートと @takazudo/zfb-md-wasm/highlight のインポートに移行は不要です。 追加された /render/parse は独立した SWC-free グラフで、swc_corezfb-render を除外しながら zfb-contentsyntect-fancy を意図的に保持します。parse は syntect-free ではありません。

インポートgzip-9 wasm(2.15.0)正確なランタイム値型の面使う場面
@takazudo/zfb-md-wasm1,516,383 BinitcompilerenderHtmlparseToAsthighlightCodeversiontoMdastRootMdastAdapterErrorZfbMdWasmTrapErrorZfbMdWasmTrapRecoveryLimitError__forceTrapForTests__getTrapRecoveryStateForTestscompile/render/parse/raw-mdast/highlight の現在の完全な型セットMDX をコンパイル、または複数の API を使う場合
@takazudo/zfb-md-wasm/highlight817,951 BinithighlightCodeversionZfbMdWasmTrapErrorZfbMdWasmTrapRecoveryLimitError__forceTrapForTests__getTrapRecoveryStateForTestsHighlightRoleHighlightCodeOptionsHighlightCodeResultHighlightDiagnosticHighlightDiagnosticSourceコードだけをハイライトする場合。公開 API / リソースは互換
@takazudo/zfb-md-wasm/render1,091,678 BinitrenderHtmlversionZfbMdWasmTrapErrorZfbMdWasmTrapRecoveryLimitError__forceTrapForTests__getTrapRecoveryStateForTestsRenderHtmlResultDiagnosticDiagnosticSourceZfbMdWasmOptionsParseDialectPipelineOptionsGfmOptionsCodeHighlightModeCodeHighlightOptionsMarkdownFeaturesConfigJsxRuntimeHighlightRoleコンパイラのバイトを含めず Markdown を HTML にする場合
@takazudo/zfb-md-wasm/parse283,991 BinitparseToAsttoMdastRootMdastAdapterErrorversionZfbMdWasmTrapErrorZfbMdWasmTrapRecoveryLimitError__forceTrapForTests__getTrapRecoveryStateForTestsParseToAstResultParseToAstOptionsParseDialectFrontmatterPolicyParsePipelineOptionsDiagnosticDiagnosticSourceAstPointAstPositionRawMdastDataMarkdownRsStopMdastNodeMdastRootUnknownMdastNodeRootParagraphHeadingThematicBreakBlockquoteListListItemHtmlCodeDefinitionTextDirectiveNodeBaseContainerDirectiveLeafDirectiveTextDirectiveEmphasisStrongInlineCodeBreakLinkImageReferenceKindLinkReferenceImageReferenceFootnoteDefinitionFootnoteReferenceTableAlignTableTableRowTableCellDeleteYamlMdxFlowExpressionMdxTextExpressionMdxJsxFlowElementMdxJsxTextElementMdxJsxAttributeContentMdxJsxAttributeMdxJsxAttributeValueExpressionMdxJsxExpressionAttributeAST をパースし、必要なら利用側の制御されたコードで解釈する場合

focused エントリは、対応する private リソースペアだけを持ちます:

wasm-render/zfb_md_wasm_render_glue.zfb-resource.mjs + zfb_md_wasm_render_bg.wasm
wasm-parse/zfb_md_wasm_parse_glue.zfb-resource.mjs + zfb_md_wasm_parse_bg.wasm

宣言サイドカーも対応ディレクトリに閉じています。各エントリは独立したコンパイル済みモジュール、 インスタンス、世代、リトライ状態、終端状態を持ちます。複数のエントリをインポートすると、独立した ペアとインスタンスが意図的にロードされます。

Node の直接利用とブラウザ対応バンドラでは、focused サブパスを選びます:

import { renderHtml } from "@takazudo/zfb-md-wasm/render";
import { parseToAst, toMdastRoot } from "@takazudo/zfb-md-wasm/parse";

ブラウザのユーザー操作から遅延インポートすると、選択したエントリのペアだけが取得されます:

renderButton.addEventListener("click", async () => {
  const { renderHtml } = await import("@takazudo/zfb-md-wasm/render");
  const { html } = await renderHtml(source, { filename: "preview.md" });
  preview.innerHTML = html ?? "";
});

parseButton.addEventListener("click", async () => {
  const { parseToAst, toMdastRoot } = await import("@takazudo/zfb-md-wasm/parse");
  const parsed = await parseToAst(source, { filename: "preview.md" });
  const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);
  inspect(root);
});

compile() はルートだけにあり、ホストによる評価、JSX ランタイム、コンポーネントを必要とする モジュールソースを返します。toMdastRoot() は、パース済みデータを利用側の制御された AST-to-React レンダラーで 解釈するためのアダプタです。slim エントリは author JavaScript を評価しません。renderHtml は サニタイザーではなく、生 HTML は信頼できないままです。MDX JSX / 式 / ESM 形状の AST ノードは 不活性なデータです。

完全なルート API

compile()renderHtml() はどちらも、markdown/MDX の source 文字列とオプション オブジェクト — すべてのフィールドは任意で、{} はすべてのデフォルトを選びます — を受け 取り、どちらも diagnostics 配列を持つ結果オブジェクトに解決します。想定される失敗は 決してスローしません。 パースエラー、不正なオプション JSON、未知のテーマなどの問題は、 すべて code / htmlnull に設定された Diagnostic[] エントリとして返され、 スローされるエラーになることはありません。(スローされる ZfbMdWasmTrapError は wasm ビルドの実際のバグを意味します — その、より限定的なケースについては、パッケージ自身の README「Error / trap / re-init contract」を参照してください。)

compile() — MDX から ES モジュール JS へ

完全な MDX → JSX → SWC → ES モジュール。結果の code は、自動 JSX ランタイムを使う MDXContent のデフォルト export(コンポーネント関数)を持つ ES モジュールソースです。

import { compile } from "@takazudo/zfb-md-wasm";

const { code, frontmatter, diagnostics } = await compile(
  "---\ntitle: Hello\n---\n\n# Welcome\n\n<Callout>Sum is {1 + 2}</Callout>\n",
  { filename: "post.mdx", jsxRuntime: "preact" },
);
// code        -> ES-module JS source (string), or null on failure
// frontmatter -> { title: "Hello" }
// diagnostics -> []

frontmatter の値は結果の frontmatter フィールドで返されます — コンテンツ内のバインディング としては公開 されません。ソース内の {frontmatter.title} ではなく、ホスト側のコード (上記の frontmatter オブジェクト)から参照してください: コンパイル済みモジュールの スコープには frontmatter 変数がないため、コンテンツ内での参照はモジュール実行時に ReferenceError をスローします。

ブラウザでコンパイル済みモジュールを評価する

compile() は、実行可能なモジュールではなく ES モジュールの ソーステキスト を返します。 blob URL と動的な import() でそれを実行可能なモジュールに変換します:

const { code, diagnostics } = await compile(source, { filename: "preview.mdx" });
if (code === null) {
  // Compilation failed — render the `diagnostics` array instead of a module.
  return;
}
const url = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
const { default: MDXContent } = await import(/* @vite-ignore */ url);
URL.revokeObjectURL(url);

// render <MDXContent components={{ Callout }} /> with your framework

PascalCase のコンポーネント(上記ソースの <Callout>)は、モジュールの components prop を 通して渡します — zfb が自身の MDX Components で使うのと 同じ規約です。

JSX ランタイムを供給する

コンパイル済みモジュールは JSX ランタイムを裸の指定子 — preact/jsx-runtime または react/jsx-runtime — でインポートするため、それを評価するページは、 import map やバンドラを通じて、それらの指定子を自分で解決しなければなりません。zfb-md-wasm は どちらのランタイムも同梱・バンドルしません。

Fragment の癖 — preact 利用者はエイリアスが 1 つ必要

zfb のエミッタは JSX の ファクトリ をあなたが選んだ jsxRuntime から取りますが、 どちらのランタイムを選んだかにかかわらず、Fragment は常に react/jsx-runtime から インポートします。これは zfb の本番エミッタの形であり — パリティとして正しく、バグでは ありません — しかしこれは、preact 利用者が react/jsx-runtime を preact 自身の ランタイムへエイリアスしなければならないことを意味します。さもなければ Fragment は 実行時に解決に失敗します:

<script type="importmap">
  {
    "imports": {
      "preact/jsx-runtime": "https://esm.sh/preact/jsx-runtime",
      "react/jsx-runtime": "https://esm.sh/preact/jsx-runtime"
    }
  }
</script>

react 利用者は react/jsx-runtime を React 自身のランタイムにマッピングするだけでよく、 エイリアスは不要です。

renderHtml() — Markdown から HTML へ

Markdown → hast → HTML 文字列で、実行時に SWC をスキップします。プレーンな markdown の プレビューだけが必要で、コンポーネントモジュールを評価する必要がないときに使ってください。

import { renderHtml } from "@takazudo/zfb-md-wasm";

const { html, frontmatter, diagnostics } = await renderHtml("Budget <8 ms\n", {
  filename: "post.md",
});
// html -> "<p>Budget &lt;8 ms</p>"

renderHtml.md から CommonMark、.mdx から MDX を推論し、明示的な dialect: "markdown" | "mdx" はどちらの有効な拡張子も上書きします。ファイル名を 省略すると <anonymous>.md が使われるため CommonMark になります。compile は MDX 専用のままで dialect を受け取って無視し、renderHtmljsxRuntime / development を受け取って無視するため、オプションオブジェクトを動的に組み立てれば 1 つで両方の階層に 使えます。

AST へのパース(parseToAst)

parseToAst() は 3 つ目のパイプライン階層で、renderHtml() より 1 段階手前に位置します: パースの直後で止まり、HTML や JS へ進む代わりに、生の mdast ツリーを JSON として返します。 ホストがツリー自体を走査または変換する必要があるとき — 目次、カスタムレンダラ、単語数 / 読了時間のメタデータ、あるいは完成した文字列ではなくドキュメントへの構造化されたアクセスを 必要とするその他のもの — に使ってください。

import { parseToAst, type ParseToAstOptions, type ParseToAstResult } from "@takazudo/zfb-md-wasm";

const result: ParseToAstResult = await parseToAst(
  "# Welcome\n\nHello **world**.\n",
  { filename: "post.md" } satisfies ParseToAstOptions,
);
// result.ast         -> { type: "root", position: {...}, children: [...] }
// result.frontmatter -> null (no frontmatter in this source)
// result.diagnostics -> []

ParseToAstOptions

parseToAst は独自の、独立した閉じたオプションドキュメントを受け取ります — compile / renderHtml のオプションの形ではありません:

  • filename — 小文字の .md または .mdx で終わる必要があります。明示的な dialect が ない場合、.md は CommonMark を、.mdx は MDX を選びます。filename を省略すると <anonymous>.mdx が使われ、したがって MDX になります。明示的な dialect はどちらの 有効な拡張子も上書きしますが、拡張子のゲート自体は免除しません。

  • dialect"markdown" または "mdx"。省略時は上記のとおり filename から推論されます。

  • directives — 汎用の remark-directive 構文(:::name / ::name / :name)をパース します。デフォルトは false。オフのときは、ディレクティブのように見えるテキストは markdown-rs がパースしたそのままで残ります。

  • frontmatter — YAML の処理ポリシー。デフォルトは "extract":

    ポリシーパースされるソースAST の yaml ノードfrontmatter の値不正な YAML
    extract(デフォルト)frontmatter を除去した本文なしパースされた JSON / nullfrontmatter 診断が 1 つ
    node論理ソース全体正規の yaml ノードパースされた JSON / nullfrontmatter 診断が 1 つ
    none論理ソース全体、全バイトを Markdown/MDX としてなし常に nullfrontmatter 診断を生成しない
  • pipeline.gfm — 5 つの真偽値 GfmOptions スイッチ(strikethroughtableautolinkLiteraltaskListItemfootnoteDefinition)のみ。parseToAst のパイプ ラインオプションは compile / renderHtml のものより厳格です: 適用できないビジター / シリアライザ専用のつまみを黙って受け入れて無視するのではなく、themecjkFriendlyhardBreakscodeHighlightfeatures をきっぱり拒否します(deny_unknown_fields)。

ParseToAstResult

interface ParseToAstResult {
  ast: MdastRoot | null; // serialized raw mdast root on success, null on failure
  frontmatter: unknown; // parsed YAML frontmatter as JSON, per the policy table above
  diagnostics: Diagnostic[]; // same Diagnostic shape as compile()/renderHtml()
}

ast は markdown-rs の生の mdast ツリーを、その serde の形を通して、開いた unist 形状の キャリアへ変換したものです — 文書化されたすべてのノードは typeposition、ノード固有の フィールドを保持しますが、これは zfb ビジター適用前 の生の出力であり、レンダリング されたパイプラインが生成する HTML/JS ではありません。

UTF-16 の位置

ツリー内のすべてのノードは position: { start, end } を持ち、それぞれ line(1 始まり)、 column(1 始まり)、offset(0 始まり)を持つ AstPoint です。columnoffsetUTF-16 コードユニットString.prototype.slice や remark/unist 自身の規約が使うのと 同じインデックス — であり、Unicode スカラー値では ありません: BMP 外のスカラー(多くの 絵文字)はサロゲートペアであるため、offset / column を 1 ではなく 2 進めます。位置は 元のソース に対して報告され、frontmatter の行も含まれます。これは、markdown-rs が実際に パースする本文からそれらの行が除去されている frontmatter: "extract" の下でも同様です。

開いたノードのユニオンは `type` だけでは絞り込めない

MdastNode は、文書化されたセットにないあらゆるノード型のキャッチオールとしてUnknownMdastNode を含み、そのキャッチオールの type フィールドは、リテラルではなく 一般的な string です。これにより、認識されないノード型が unknown にフォールバックせず 型付けされたまま保たれますが、同時に、単純な等価チェックではキャッチオールを 絞り込んで 除外できない ことも意味します:

// child: MdastNode
if (child.type === "heading") {
  child.depth; // still a type error — TypeScript can't rule out UnknownMdastNode here
}

実行時に形を確認したうえでノードの種類を明示的にアサートするか、下記の toMdastRoot() を 使ってください。これはキャストなしで正しく絞り込めるツリーを返します。

toMdastRoot() — 検証付きで型を絞り込むアダプタ

parseToAst の生の ast は設計上、前方互換です: 認識されないノードは、呼び出しを壊すの ではなく UnknownMdastNode として残ります。toMdastRoot() はその逆の、より厳格なツール です — 生のツリーを検証し、切り離されたエコシステム形状の mdastRoot を返します。これは @types/mdast の利用者が期待するとおりに type で絞り込めます:

import { parseToAst, toMdastRoot, MdastAdapterError } from "@takazudo/zfb-md-wasm";

const { ast } = await parseToAst("# Welcome\n\nHello **world**.\n");
try {
  const root = toMdastRoot(ast);
  const heading = root.children[0];
  if (heading.type === "heading") {
    heading.depth; // narrows correctly — no assertion needed
  }
} catch (error) {
  if (error instanceof MdastAdapterError) {
    // error.path     -> e.g. "$.children[0]"
    // error.nodeType -> the offending node's `type`, or null for a null ast
  }
}

toMdastRoot() は診断する代わりに MdastAdapterError.path.nodeType を持つ TypeError のサブクラス)を スロー します — 結果オブジェクトは返しません。次の場合に スローします:

  • nullast(path は "$")、および

  • 未知またはサポートされていないノード型(mathtomlmdxjsEsm を明示的に含む)。

スローするため、パース失敗が想定される場合は必ず先に diagnostics を確認してください — toMdastRoot() は、既に成功した ast を絞り込み可能なツリーに変換するためのものであり、 パース失敗とアダプタ失敗を区別するためのものではありません。

remark からの文書化された相違点

  • mdxJsxAttribute(およびその値属性 / 式属性の兄弟)は position を持ちません — markdown-rs は属性の位置をモデル化しません。

  • トップレベルの import / export はプレーンな段落へと格下げされます(mdxjsEsm ノード なし) — wasm の境界は JS の ESM/acorn パーサをホストできません。remark-mdx 相当の ESM/estree データが必要な利用者は、それらのドキュメントには remark を使い続けてください。

  • _markdownRsStops(MDX の式 / ESM 形状のノードが持つ)は markdown-rs 内部の管理情報で、 不安定であり、UTF-8 バイト ベースです — position とは異なり、上記の UTF-16 コード ユニットの契約を共有しません。これで文字列をスライスしては決していけません。

Vite で使う

parseToAst()(および他のすべての @takazudo/zfb-md-wasm エクスポート)は、動作に Vite の プラグイン、エイリアス、設定を必要としません — 素の vite devDependency で十分です。 パッケージの browser export 条件と、グルー / wasm リソースに対する ?url アセット インポートの契約は、どちらも Vite のデフォルトの dev サーバーと本番ビルドの下で(そして zfb 自身の esbuild パイプラインの下でも同様に)自動的に解決されます。本番では サーバー MIME の要件 が引き続き適用 されます: 出力される .mjsapplication/javascript として、.wasmapplication/wasm として配信してください。

直接的なセマンティックコードハイライト

highlightCode() は、任意の HTML、CSS、JavaScript、またはその他の同梱された構文に対する、 直接的なルート API です。Markdown のフェンスを必要とせず、インラインの色や Shiki のクラスを 出力することもありません。エスケープされた、セマンティックなクラスモードの HTML を返します。

import {
  highlightCode,
  type HighlightCodeOptions,
  type HighlightCodeResult,
} from "@takazudo/zfb-md-wasm";

const output: HighlightCodeResult = await highlightCode("const answer = 42;", {
  language: "javascript", // required
  mode: "class", // optional; the only accepted mode
  classPrefix: "hi-", // optional; defaults to "hi-"
  roleClasses: { keyword: "text-violet-600 dark:text-violet-400" },
} satisfies HighlightCodeOptions);
type HighlightRole =
  | "escape"
  | "operator"
  | "comment"
  | "string"
  | "number"
  | "constant"
  | "keyword"
  | "function"
  | "type"
  | "namespace"
  | "property"
  | "variable"
  | "tag"
  | "attribute"
  | "punctuation"
  | "inserted"
  | "deleted"
  | "heading";

interface HighlightCodeOptions {
  language: string;
  mode?: "class";
  classPrefix?: string;
  roleClasses?: Partial<Record<HighlightRole, string>>;
}
interface HighlightCodeResult {
  html: string | null;
  diagnostics: HighlightDiagnostic[];
}

デフォルトの出力は <pre class="hi-root"><code>…</code></pre> で、空でない各行に <span class="line"> が付きます。固定されたフルネームのロールキーとデフォルトの hi- クラスは次のとおりです: escapehi-escoperatorhi-opcommenthi-comstringhi-strnumberhi-numconstanthi-constkeywordhi-kwfunctionhi-fntypehi-tynamespacehi-nspropertyhi-propvariablehi-vartaghi-tagattributehi-attrpunctuationhi-punctinsertedhi-insdeletedhi-delheadinghi-hd

roleClasses のキーは、そのリストのフルネームです。例えば { keyword: "my-keyword" }hi-kw を置き換えます。kw は有効な上書きキーではありません。classPrefix は、ルート ("token-" の場合は token-root)と、上書きされていないすべてのトークンクラスの両方を 変更します。

無効なオプション(例えば language の欠落、サポートされないモード、無効なプレフィックス、 認識されないロールキー、余分なプロパティ)は、source: "options"error 診断とともに html: null を返します。スローはしません。未知だが空でない language は、source: "highlight"warning 診断 1 つと null の位置フィールドを伴う、成功したエスケープ済みフォールバックに なります。不完全なエディタ入力は受け入れられ、診断なしで通常の markup を返すことがあります。

遅延ロードされるブラウザリソースとサーバー MIME

すべてのパッケージエントリは、自分自身の glue/wasm ペアだけを静的に宣言します。ルートは wasm/zfb_md_wasm_glue.zfb-resource.mjswasm/zfb_md_wasm_bg.wasm を保持し、focused ペアは次のとおりです:

wasm-render/zfb_md_wasm_render_glue.zfb-resource.mjs
wasm-render/zfb_md_wasm_render_bg.wasm
wasm-parse/zfb_md_wasm_parse_glue.zfb-resource.mjs
wasm-parse/zfb_md_wasm_parse_bg.wasm

zfb の本番ビルドは、選択したペアをハッシュ付き island アセットとして出力します:

assets/islands-resource-zfb_md_wasm_glue.zfb-resource-<hash>.mjs
assets/islands-resource-zfb_md_wasm_bg-<hash>.wasm

focused アセットは、stem に render または parse を含む同じ命名規則になります (例: islands-resource-zfb_md_wasm_render_bg-<hash>.wasm)。

./highlight サブパス は代わりに、独自の別のリソースのペアを宣言します — zfb_md_wasm_highlight_glue.zfb-resource.mjszfb_md_wasm_highlight_bg.wasm — で、 同じ方法でハッシュ化されます:

assets/islands-resource-zfb_md_wasm_highlight_glue.zfb-resource-<hash>.mjs
assets/islands-resource-zfb_md_wasm_highlight_bg-<hash>.wasm

同じバンドルで複数のエントリをインポートすると、各 private ペアと独立したコンパイル済み モジュール / インスタンス状態が意図どおりロードされます。可能ならバンドルごとに 1 つを選び、 両方の機能が必要な場合は複数利用できます。

初期ページロードから除外するには、ユーザー操作からのみ選択したエントリを動的にインポートします。 その最初の公開 API 呼び出しで、グルーと wasm がフェッチされます:

button.addEventListener("click", async () => {
  const { renderHtml } = await import("@takazudo/zfb-md-wasm/render");
  const result = await renderHtml(editor.value, { filename: "preview.md" });
  preview.innerHTML = result.html ?? "";
});

parseButton.addEventListener("click", async () => {
  const { parseToAst, toMdastRoot } = await import("@takazudo/zfb-md-wasm/parse");
  const parsed = await parseToAst(editor.value, { filename: "preview.md" });
  const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);
  inspect(root);
});

出力される .mjsapplication/javascript として、.wasmapplication/wasm として 配信してください。リソースを手動でコピーしたり、パッケージのソースパスをインポートしたり しないでください: zfb がハッシュ付きの URL グラフを維持できるよう、パックされたブラウザ エントリを使ってください。

出荷アーティファクトのサイズと上限

出荷された 2.15.0 のアーティファクトの行は次のとおりです。final wasm は wasm-bindgen と wasm-opt を通した最適化後の値、gzip-9 は Node の gzipSync(..., { level: 9 })、glue は グルーコードのバイト数とその gzip です:

エントリ / グラフfinal wasmgzip-9glueglue gzip-9
root(full)3,399,954 B1,516,383 B14,998 B4,199 B
highlight1,539,334 B817,951 B8,758 B2,637 B
render2,196,095 B1,091,678 B8,772 B2,661 B
parse700,364 B283,991 B11,159 B3,797 B

#2447 の決定スナップショットでは、split パッケージの構成が 3,638,607 B、root + highlight が 2,314,818 B、クリーンな本番の参照上限は 210 秒、選択されたスナップショットの中央値は 155.015 秒 [153.496, 165.977] でした。固定された gzip-9 の上限は root 1,600,000 B、highlight 880,000 B、 render 1,100,000 B、parse 325,000 B、完全な packed tarball は 3,900,000 B です。4 つのエントリは いずれも上限の内側で出荷されており、上限までの余裕は root 83,617 B、highlight 62,049 B、 render 8,322 B、parse 41,009 B です。これらは永続的な保証ではなく 2.15.0 の測定値です — 実際にインストールしたバージョンで測り直してください。

サイズは保証されますが、コンテンツダイジェストは保証されません。 上記のバイトサイズは shipped-sizes.json が保持し CI がアサートしているため、意図的なアーティファクト変更が あったときにしか動きません。SHA-256 ダイジェストは事情が異なります。各リリースは自身の バージョン文字列を個々の .wasm に焼き込む(version() が返す値です)ため、コンパイル結果が 同一でバイトサイズがまったく動かないドキュメントのみのパッチであっても、4 つのダイジェストは 毎リリース変わります。semver ではなくコンテンツダイジェストでこれらのアーティファクトを 検証している場合は、アップグレードのたびに再ピン留めしてください。「サイズが変わっていない」を 「再検証は不要」と読み替えてはいけません。

highlight グラフから swc_core をゲートで外したこと(#2449 / #2450)は、 証明可能性の勝利であって、サイズの勝利ではありません。#2447 の SWC 保持ベースラインは raw 1,484,705 B、gzip-9 767,009 B で、#2450 の結果は raw で 7,965 B、gzip-9 で 8,765 B(結果は 758,244 B)小さくなりました。wasm-opt は 到達不能な swc_core を既にデッドストリップしていたのであり、#2450 の完全パリティと swc_core 不在のアサーションが、その創発的な性質を保証されたものへと変えました。 highlight だけを使う利用者にとって意味のある差は、root と highlight の差のほうです: raw で 1,860,620 B、gzip-9 で 698,432 B 小さく、highlight は root の raw バイトの約 45%、 gzip 後のバイトの約 54% に収まります。

Node での利用(テストとツール)

エントリは追加のセットアップなしで Node ≥ 20 の下で動作します。ツールやスナップショット テストでは、処理に対応するリソースペアを直接選びます:

import { renderHtml } from "@takazudo/zfb-md-wasm/render";
import { parseToAst, toMdastRoot } from "@takazudo/zfb-md-wasm/parse";

const { html } = await renderHtml("# Hello from Node\n");
const parsed = await parseToAst("# Hello from Node\n", { filename: "post.md" });
const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);

同じ利用者が compile も必要とする場合はルートを使います。ルートと highlight のインポートは後方互換です。

パリティ保証と制限

出力は、固定されたフィクスチャコーパスにおいて zfb のネイティブパイプラインと一致します — パリティテストスイートは完全一致でゲートします。この保証には、ブラウザビルドに対する意図 的な制限が伴います:

  • ファイルシステムに依存するプラグインは不活性です。 transcludeimageDimensionslinkValidation は登録されますが、ファイルシステムには一切触れません — ブラウザのタブ にはファイルシステムがありません — これは、zfb 自身の MDX ローダがビルドコンテキストの ルートを無効化した状態でそれらを実行するのとまったく同じです。ホストコールバック版 (ブラウザのホストが必要に応じてファイル内容を供給できるようにするもの)は将来の epic の 可能性であり、ここでは実装されていません。

  • 設定は zfb.config.ts ではなく、解決済みの JSON です。 TypeScript の設定を評価する には JS エンジンが必要であり、それはビルド側にとどまります。まず設定を JSON へ解決し — zfb がビルド時に zfb.config.ts から導出するのと同じ形、Markdown のカスタマイズ を参照 — それを pipeline オプションとして渡してください。

  • クロスファイル機能はありません。 ルートテーブルのリンク解決とクロスファイルの アンカー解決には、プロジェクト全体のルートグラフが必要ですが、単一ドキュメントのブラウザ 呼び出しはそれを決して持ちません。

  • ルートは互換性 / コンパイラのエントリです。 compile は author JavaScript を実行し得る 唯一の経路なので、完全なグラフを保持します。SWC-free な処理には /render または /parse、 直接的なハイライトには /highlight を選んでください。focused エントリは意図的に zfb-content / syntect-fancy を保持し、parse は syntect-free ではありません。

  • renderHtml はサニタイザーではありません。 生 HTML は信頼できないままです。MDX の JSX / 式 / ESM 形状の AST ノードは不活性なデータであり、既にパースされた AST を解釈できるのは 利用側の制御されたコードだけです。slim エントリは author JavaScript を評価しません。

  • renderHtml はファイル名から構文を選びます。 .md は CommonMark、.mdx は MDX を 使い、明示的な dialect はどちらの有効な拡張子も上書きします。compile は MDX 専用の ままです。

  • シンタックスハイライトは syntect の fancy-regex バックエンドを使います — ネイティブ zfb の oniguruma バックエンドではありません(oniguruma は wasm にコンパイルできません)。 ネイティブ側の挙動については シンタックスハイライト を参照 してください。2 つのバックエンドは zfb のフィクスチャコーパスにおいてバイト単位で同一 です。文法レベルの相違は、クレート自身の情報提供用のバックエンド相違テストによって追跡 されます。

バンドルサイズ

出荷された 2.15.0 のアーティファクトの行は次のとおりです。final wasm は wasm-bindgen と wasm-opt を通した最適化後の値、gzip-9 は Node の gzipSync(..., { level: 9 })、glue は グルーコードのバイト数とその gzip です:

エントリ / グラフfinal wasmgzip-9glueglue gzip-9
root(full)3,399,954 B1,516,383 B14,998 B4,199 B
highlight1,539,334 B817,951 B8,758 B2,637 B
render2,196,095 B1,091,678 B8,772 B2,661 B
parse700,364 B283,991 B11,159 B3,797 B

#2447 の決定スナップショットでは、split パッケージの構成が 3,638,607 B、root + highlight が 2,314,818 B、クリーンな本番の上限は 210 秒、決定スナップショットの中央値は 155.015 秒 [153.496, 165.977] でした。固定された gzip-9 の上限は root 1,600,000 B、highlight 880,000 B、 render 1,100,000 B、parse 325,000 B、完全な packed tarball は 3,900,000 B です。4 つのエントリは いずれも上限の内側で出荷されており、上限までの余裕は root 83,617 B、highlight 62,049 B、 render 8,322 B、parse 41,009 B です。これらは永続的な保証ではなく 2.15.0 の測定値です — 実際にインストールしたバージョンで測り直してください。

出荷アーティファクトのサイズと上限 にあるダイジェストの注意書きは、この表にも当てはまります。ここに挙げたバイトサイズは 保証されますが、アーティファクトの SHA-256 ダイジェストは毎リリース変わります。

highlight グラフから swc_core をゲートで外したこと(#2449 / #2450)は、 証明可能性の勝利であって、サイズの勝利ではありません。#2447 の SWC 保持ベースラインは raw 1,484,705 B、gzip-9 767,009 B で、#2450 の結果は raw で 7,965 B、gzip-9 で 8,765 B(結果は 758,244 B)小さくなりました。wasm-opt は 到達不能な swc_core を既にデッドストリップしていたのであり、#2450 の完全パリティと swc_core 不在のアサーションが、その創発的な性質を保証されたものへと変えました。 highlight だけを使う利用者にとって意味のある差は、root と highlight の差のほうです: raw で 1,860,620 B、gzip-9 で 698,432 B 小さく、highlight は root の raw バイトの約 45%、 gzip 後のバイトの約 54% に収まります。

トラップからの回復

想定される highlightCode のオプションエラーや未知の language のフォールバックは、通常の 結果診断です。本物の wasm トラップは別です: 該当インスタンスは汚染されるため、その呼び出しは ZfbMdWasmTrapError をスローします。ラッパーは、キャッシュ済みのコンパイル済み wasm モジュールから直ちに新しいインスタンスを起動するため、次の compilerenderHtmlhighlightCodeversion の呼び出しは、2 回目の wasm の fetch / コンパイルなしに置き換え後 のインスタンスを使います。ブラウザでの回復は、新しいグルーモジュールの世代 (?zfbMdWasmGen=N)をインポートし、無制限なモジュールレコードを避けるため 16 回の回復に 上限が設けられています。トラップは常にパッケージのバグです。それを引き起こした入力を報告 してください。

関連項目

  • Playground — ブラウザで Rust パイプラインをインタラクティブに試せます。

  • MDX Componentscompile() の出力が従う components prop の規約。

  • Markdown のカスタマイズpipeline オプションが反映する パイプライン設定を zfb がどう解決するか。

  • シンタックスハイライト — wasm ビルドの fancy-regex バックエンドと比較するための、ネイティブ zfb の syntect のセットアップ。

  • @takazudo/zfb-md-wasm API リファレンスcompilerenderHtmlparseToAsttoMdastRoothighlightCode の完全なオプション / 結果の型リファレンス。

  • Wasm のインポート — このページのブラウザ側の使い方と比較する ための、SSR 側での Wasm モジュールのロード。

Revision History

作成更新