ブログ一覧

BudouX をサーバー環境で使うと window is not defined になる — DOM 非依存の Parser を直接 import する

日本語サイトの見出しやタイトルの改行位置を自然にするため、Google の BudouX を使っています。機械学習ベースの分かち書きライブラリで、私は/犬と/散歩に/行った のような意味の切れ目でテキストを分割してくれます。

これを Astro の SSR(Cloudflare Workers 上)で使おうとしたら、import しただけでこうなりました。

ReferenceError: window is not defined

window なんて一行も書いていないのに、です。本記事では原因(パッケージのエントリポイントが DOM 依存コードを巻き込む構造)と、DOM 非依存の Parser クラスを直接 import する回避策をまとめます。

前提: budoux 0.6.4 で確認。Cloudflare Workers に限らず、Node.js の SSR や Edge Runtime など「DOM のないサーバー環境」全般で同じことが起きます。

原因: エントリポイントが HTML 処理系を巻き込む

budoux の package.jsonmodule: "./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> = {
	"&": "&amp;",
	"<": "&lt;",
	">": "&gt;",
	'"': "&quot;",
	"'": "&#39;",
};

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.jsParserbudoux/module/data/models/ja.js のモデルを直接 import すれば DOM 非依存で動く
  • ついでに不要な他言語モデルもバンドルから消えて軽くなる
  • <wbr> + word-break: keep-all で改行制御。set:html に渡すなら HTML エスケープを忘れずに

参考リンク