「Missing Exports」を超えて:早期ガベージコレクションの実装
Webpackのドキュメントにおけるリンク切れを修正するために、標準的なプラグインを使おうとしましたが、失敗しました。
大規模なコードベースにプラグインを統合するのは、簡単なことではありません。それはアーキテクチャ上の課題となりました。Abstract Syntax Tree (AST) の操作、メモリ使用量、そして再帰ループを管理しなければなりませんでした。
標準的なプラグインの問題点
解決策を見つけるために、3つの実験を行いました。
実験1:プラグインは135個の型を復元しました。しかし、それらは内部モジュールに配置されてしまいました。私たちのツールはそれらのために別のフォルダを作成したため、手動で整理する必要がありました。また、ドキュメントの構造も正しくありませんでした。
実験2:外部の型を非表示にする設定を有効にしました。これにより一部の課題は解決しました。ペイロードは60個のWebpackの型まで削減されました。しかし、TypeDocはネストされた依存関係を無視してしまいました。その結果、多くの型が非表示のままメモリを消費し続けることになりました。
実験3:型を元のモジュールにマッピングし直そうと試みました。これが再帰ループを引き起こしました。プラグインはネストされたインターフェースを抽出し続け、最終的に630個もの型が生成されました。ドキュメントはノイズだらけになり、ユーザーエクスペリエンスを損なう結果となりました。
解決策:早期ガベージコレクション
余計なノイズを排除しつつ、型を正しいモジュールに配置する必要がありました。私は出力結果を修正しようとするのをやめ、プロセスそのものを修正することに注力しました。
EVENT_RESOLVE_END というフックを使用しました。これにより、解決(resolution)の直後にASTをインターセプトできるようになりました。TypeDocがカテゴリを割り当てたり、大量のメモリを消費したりする前にこれを行うようにしました。
ロジックは以下の3つのステップに従いました:
- AST内の内部モジュールを見つける。
- カスタムユーティリティを使用して、すべてのノードをチェックする。
- ノイズを削除する。project.removeReflection を使用して、不要な300個のノードを即座に削除しました。これにより、Node.jsがメモリを解放できるようになりました。
- 重要な型を移動する。残りの300個の型をルートスコープにマージしました。
結果:240個の不可欠な型を救い出すことができました。これらは現在、正しく表示され、適切にルーティングされています。実験3で見られた再帰的なノイズも回避できました。
学んだ教訓
• ASTは早い段階で処理する。不要なノードを削除することで、後のメモリの浪費を防ぐことができます。 • ハックではなくフックを使用する。コンパイラのライフサイクルを理解することで、クリーンな実装が可能になります。 • フィードバックがアーキテクチャを改善する。メンテナーによるレビューのおかげで、より良いシステムを構築できました。 • データが多ければ良いわけではない。優れたエンジニアリングとは、データと使いやすさのバランスを見つけることです。
Missing Exportsを超えて:WebpackのTypeDoc AST向けに早期ガベージコレクタを構築する
TypeDocを使用してドキュメントを生成する際、しばしば「Missing Exports(エクスポートの欠落)」という問題に直面します。これは、コード内で定義されているものの、実際には外部に公開(エクスポート)されていないシンボルが、ドキュメントに含まれてしまうことを指します。
大規模なTypeScriptプロジェクトにおいて、これは単なるノイズ以上の問題となります。ドキュメントの精度が低下し、開発者が「実際に利用可能なAPI」と「内部実装用のシンボル」を区別するのが困難になるからです。
この記事では、WebpackのTypeDoc AST(抽象構文木)において、不要なノードを早期に特定して除去するための「早期ガベージコレクタ」を構築するプロセスについて解説します。
問題の核心:ASTの肥大化
TypeDocは、TypeScriptのソースコードを解析してASTを構築します。しかし、標準的なプロセスでは、ASTに含まれるすべてのノードがドキュメント化の対象となる可能性があります。
たとえある関数が export されていなくても、その関数がAST内に存在する場合、TypeDocのプロセスはそれを「検討すべき対象」として扱います。その結果、以下のような問題が発生します:
- ドキュメントの汚染: 公開APIではない内部的なヘルパー関数や型定義がドキュメントに表示される。
- パフォーマンスの低下: 不要なノードを処理するために、メモリとCPUリソースが無駄に消費される。
- Webpackとの不整合: Webpackのバンドルプロセスでツリーシェイキング(Tree Shaking)によって削除されるコードが、ドキュメントには残ってしまう。
早期ガベージコレクタのコンセプト
通常、ガベージコレクション(GC)はメモリ管理のために実行されますが、ここで提案する「早期ガベージコレクタ」は、ASTの走査(Traversal)の極めて早い段階で、ドキュメント化に不要なノードを「マークして削除」する仕組みを指します。
このアプローチは、古典的な「マーク・アンド・スイープ(Mark-and-Sweep)」アルゴリズムに基づいています。
- マーク(Mark): エクスポートされているルートノードから開始し、到達可能な(Reachable)すべてのノードをマークする。
- スイープ(Sweep): マークされていない(到達不能な)すべてのノードを、ASTから削除する。
実装戦略
Webpackのプラグインエコシステム内でこれを実現するには、TypeDocがASTを構築した直後、かつドキュメント生成のメインループが始まる前に介入する必要があります。
1. ASTの走査
まず、TypeDocが提供するASTを再帰的に走査します。
function traverse(node: TypeDocNode) {
// ノードの処理
for (const child of node.children) {
traverse(child);
}
}
2. 到達可能性の判定
次に、各ノードが「エクスポートされたエントリポイント」から辿れるかどうかを判定します。
const reachableNodes = new Set<string>();
function mark(node: TypeDocNode) {
if (reachableNodes.has(node.id)) return;
reachableNodes.add(node.id);
for (const child of node.children) {
mark(child);
}
}
// エクスポートされたルートから開始
exportNodes.forEach(node => mark(node));
3. 不要なノードの除去
最後に、reachableNodes に含まれていないノードをツリーから切り離します。
function sweep(node: TypeDocNode) {
node.children = node.children.filter(child => {
if (!reachableNodes.has(child.id)) {
return false; // ノードを削除
}
sweep(child);
return true;
});
}
Webpackとの統合
このロジックをWebpackのカスタムプラグインとして実装することで、ビルドパイプラインの一部として自動的に実行できます。Webpackがモジュールグラフを構築する際、TypeDocのASTに対しても同様の最適化を適用することで、ドキュメント生成の効率を劇的に向上させることができます。
結論
「Missing Exports」の問題は、単にドキュメントから項目を隠すことではなく、ASTの構造自体をクリーンに保つことで解決すべき課題です。早期ガベージコレクタを導入することで、より正確で、より軽量な、そして開発者にとって真に価値のあるドキュメントを生成することが可能になります。
これにより、ドキュメントは「コードの全容」ではなく、「公開されたインターフェースの真の姿」を反映するものへと進化します。