Skip to content

処理の重い entry を責務境界で分離 + デフォルトでサニタイズ・ class-only highlight に (major redesign) #36

@sor4chi

Description

@sor4chi

背景

このパッケージを SPA に組み込んで動かしてみたところ、 以下 3 点が同時に問題として浮上したので、 まとめて redesign の提案を立てます。 破壊的変更ありの 3.0 想定。

問題

1. /server /client という命名が責務を表していない

どちらも実体は isomorphic な ESM で、 環境的に server-only / client-only ではない (CF Workers 対応の issue #21 でも browser/edge で動かす前提が出ている) 。 命名上は 「server なら重い処理を持ち込んでもよい」 ように見えるが、 実際には consumer が /server を import するかどうか が bundle の重さを決める唯一の境界。 SSG では build step だけで markdown を扱い、 client には持ち込みたくないケースが多い — その意図を import path で物理的に表現できる構造が要る。

2. shiki がフル bundle で抱え込まれていて重い

Vite SPA で parseMarkdownToHTML を import + 実呼び出しした際の計測:

entry (raw / gzip) dist 全体 chunk 数
未 import 565 KB / 178 KB 428 KB 4
import のみ (副作用無し) 588 KB / 186 KB 660 KB 8
実呼び出しあり 1,212 KB / 375 KB 10 MB 304

src/server/shiki.tsbundledLanguagesInfo / bundledThemesInfo を全食いしているため、 Vite が shiki/langs/*.mjs を全部 dynamic chunk として焼く。 lazy fetch なので初回 entry は 1.2 MB / gzip 375 KB に収まるものの、 これでも実用には重く、 deploy artifact は 10 MB に膨れる。

3. デフォルト設定だと markdown URL スキームに XSS が残る

unified パイプラインは remark-rehype のデフォルトで生 HTML を落とすが、 markdown 構文経由の link/image の URL スキームは検証しない:

[a](javascript:alert(1))   → <a href="javascript:...">
![](javascript:alert(1))   → <img src="javascript:...">
<javascript:alert(1)>      → autolink <a href="javascript:...">

mdast-util-to-hastdefaultProtocols は表に出ていないし、 そもそも rehype-sanitize を末尾に置けば済む話だが、 shiki の inline style 属性のために defaultSchema がそのままでは使えない (style を strip すると highlight が消える) 。

提案 (3.0 redesign)

a. subpath を責務ベースに

中身
/server /processor (仮) parse / shiki / 全部入りパイプライン (重い側)
/client . (root) render-only helper / 型 / 軽量 utils
/style.css highlight 用 stylesheet (class-only shiki)

「うっかり client から processor を引っ張る」 を import path で物理隔離。

b. shiki を pin できる factory に

import { createMarkdownProcessor } from "@saitamau-maximum/markdown-processor/processor";

const processor = await createMarkdownProcessor({
  langs: ["ts", "tsx", "js", "jsx", "json", "md", "sh", "go"],
  themes: ["github-dark", "github-light"],
});
  • 内部で shiki/core + createHighlighterCore に切り替え、 言語/theme を consumer 注入
  • bundledLanguages 一括 import は default ではなく preset entry (/processor/full) として残し、 既存利用者の移行を楽にする

c. shiki の inline style を class 化

shiki の transformerStyleToClass を default で適用:

  • token 単位で deterministic な hash class (__maximum_md_xxx) に変換
  • getCSS() で取り出した stylesheet を ./style.css として export (もしくは getStylesheet() runtime API)
  • 結果として style 属性が出力に出なくなる

d. rehype-sanitize をデフォルト pipeline に

c によって style を保持する必要が無くなるので、 defaultSchema をそのまま末尾に挿せる。 href / src のスキーム whitelist も含まれているため、 consumer 側のサニタイズは不要に。 脱出ハッチとして sanitize: false または sanitizeSchema で上書き可。

影響範囲・移行

  • 破壊的変更: import path 変更 + 行動変化 (デフォルト sanitize, class-only highlight)
  • 移行ガイド: /server/processor, /client., または /processor/full で旧挙動互換

進め方の提案

  1. この issue で方向性合意
  2. 別 PR で:
    • shiki factory 化 (#b)
    • transformerStyleToClass 適用 (#c)
    • rehype-sanitize 組み込み (#d)
    • subpath 再編 (#a)
  3. 3.0 リリース → 各 consumer 移行 PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions