背景
このパッケージを 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.ts で bundledLanguagesInfo / 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:...">
) → <img src="javascript:...">
<javascript:alert(1)> → autolink <a href="javascript:...">
mdast-util-to-hast の defaultProtocols は表に出ていないし、 そもそも 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 で旧挙動互換
進め方の提案
- この issue で方向性合意
- 別 PR で:
- shiki factory 化 (#b)
- transformerStyleToClass 適用 (#c)
- rehype-sanitize 組み込み (#d)
- subpath 再編 (#a)
- 3.0 リリース → 各 consumer 移行 PR
背景
このパッケージを 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 + 実呼び出しした際の計測:src/server/shiki.tsでbundledLanguagesInfo/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 スキームは検証しない:mdast-util-to-hastのdefaultProtocolsは表に出ていないし、 そもそもrehype-sanitizeを末尾に置けば済む話だが、 shiki の inlinestyle属性のためにdefaultSchemaがそのままでは使えない (style を strip すると highlight が消える) 。提案 (3.0 redesign)
a. subpath を責務ベースに
/server/processor(仮)/client.(root)/style.css「うっかり client から processor を引っ張る」 を import path で物理隔離。
b. shiki を pin できる factory に
shiki/core+createHighlighterCoreに切り替え、 言語/theme を consumer 注入bundledLanguages一括 import は default ではなく preset entry (/processor/full) として残し、 既存利用者の移行を楽にするc. shiki の inline style を class 化
shiki の
transformerStyleToClassを default で適用:__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で上書き可。影響範囲・移行
/server→/processor,/client→., または/processor/fullで旧挙動互換進め方の提案