BudouX をサーバー環境で使うと window is not defined になる — DOM 非依存の Parser を直接 import する
日本語サイトの見出しやタイトルの改行位置を自然にするため、Google の BudouX を使っています。機械学習ベースの分かち書きライブラリで、私は/犬と/散歩に/行った のような意味の切れ目でテキストを分割してくれます。
これを Astro の SSR(Cloudflare Workers 上)で使おうとしたら、import しただけでこうなりました。
ReferenceError: window is not definedwindow なんて一行も書いていないのに、です。本記事では原因(パッケージのエントリポイントが DOM 依存コードを巻き込む構造)と、DOM 非依存の Parser クラスを直接 import する回避策をまとめます。
前提: budoux 0.6.4 で確認。Cloudflare Workers に限らず、Node.js の SSR や Edge Runtime など「DOM のないサーバー環境」全般で同じことが起きます。
原因: エントリポイントが HTML 処理系を巻き込む
budoux の package.json は module: "./module/index.js" を指しており、この index.js が全部入りです。
// budoux/module/index.js(抜粋)
import { model as jaModel } from './data/models/ja.js';
import { model as zhHansModel } from './data/models/zh-hans.js';
import { model as zhHantModel } from './data/models/zh-hant.js';
import { model as thModel } from './data/models/th.js';
// ...
import { HTMLProcessingParser } from './html_processor.js';問題はこの html_processor.js です。HTML 文字列に直接 <wbr> を差し込むための DOM 処理系で、依存の先に win.js というファイルがいます。
// budoux/module/win.js
export const win = window;モジュールのトップレベルで `window` を参照しているので、バンドルに含まれた状態でサーバー環境が import を評価した瞬間、使う・使わないに関係なく window is not defined で死にます。自分のコードに window がなくても発生するのはこのためです。
解決: Parser とモデルをサブパスから直接 import する
分かち書きだけが目的なら、DOM 処理系は不要です。budoux は純粋な Parser クラスと言語モデルを個別ファイルで公開しているので、index を経由せず直接 import します。
// budoux の main (index) は DOM 処理系(win.js)を巻き込み、サーバー環境で
// window is not defined になる。Parser とモデルを直接 import して回避する。
import { Parser } from "budoux/module/parser.js";
import { model as jaModel } from "budoux/module/data/models/ja.js";
const parser = new Parser(jaModel);
parser.parse("私は犬と散歩に行った");
// => ["私は", "犬と", "散歩に", "行った"]Parser は文字列を受けて文字列配列を返すだけの純粋なクラスなので、Workers でも Node でも動きます。
副次的なメリットもあります。index.js は日本語のほかに中国語(簡体・繁体)とタイ語のモデルも全部 import しているので、日本語しか使わないなら直接 import の方がバンドルも軽くなります。
実装例: 分割点に <wbr> を入れて改行を制御する
セグメント配列をそのまま <wbr>(改行してよい位置を示すタグ)で連結し、CSS の word-break: keep-all と組み合わせるのが定番です。
const ESCAPE: Record<string, string> = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (ch) => ESCAPE[ch]);
}
export function budoux(text: string | null | undefined): string {
if (!text) return "";
return parser.parse(text).map(escapeHtml).join("<wbr>");
}.bd {
word-break: keep-all; /* <wbr> の位置でだけ改行させる */
}Astro なら <h1 class="bd" set:html={budoux(title)} /> のように使います。set:html に渡す前提なので、セグメントごとの HTML エスケープを忘れないでください(CMS 由来のテキストをそのまま流すと XSS になります)。
まとめ
- budoux のエントリポイント(
module/index.js)は DOM 依存の HTML 処理系を巻き込んでおり、その先のwin.jsがトップレベルで `window` を参照しているため、サーバー環境では import しただけで死ぬ - 分かち書きだけなら
budoux/module/parser.jsのParserとbudoux/module/data/models/ja.jsのモデルを直接 import すれば DOM 非依存で動く - ついでに不要な他言語モデルもバンドルから消えて軽くなる
<wbr>+word-break: keep-allで改行制御。set:htmlに渡すなら HTML エスケープを忘れずに