[basecamp/lexxy] Lexical拡張機能のためのプラガブルなExtension APIの導入

basecamp/lexxy

Context: 拡張可能なエディタアーキテクチャの必要性

Lexxyは、Lexicalベースのリッチテキストエディタとして、多様な機能要件に応えるため拡張可能なアーキテクチャが求められていました。この変更では、下流ユーザーがLexicalの動作を簡単にカスタマイズし、ツールバーにマークアップを追加できる仕組みを導入しています。

従来は、エディタの振る舞いを拡張するためにはLexxyのコアコードを修正する必要がありましたが、このPRで導入されたExtension APIにより、プラガブルな形で機能を追加できるようになりました。

Technical Detail: Extension APIの実装

1. Extension基底クラスの設計

新しい LexxyExtension 基底クラスが追加され、拡張機能の標準インターフェースが定義されました。

export default class LexxyExtension {
  #editorElement

  constructor(editorElement) {
    this.#editorElement = editorElement
  }

  get editorElement() {
    return this.#editorElement
  }

  get editorConfig() {
    return this.#editorElement.config
  }

  // optional: defaults to true
  get enabled() {
    return true
  }

  get lexicalExtension() {
    return null
  }

  initializeToolbar(_lexxyToolbar) {
    // 拡張ごとに実装
  }
}

各拡張は以下の機能を持ちます:

  • enabled getter: 拡張を有効にするかどうかを動的に判定
  • lexicalExtension getter: Lexicalエディタに渡す拡張定義を返す
  • initializeToolbar() メソッド: ツールバーのカスタマイズロジックを実装

2. Extensions管理クラスの実装

Extensions クラスが、エディタごとに登録された拡張機能のライフサイクルを管理します。

export default class Extensions {
  constructor(lexxyElement) {
    this.lexxyElement = lexxyElement
    this.enabledExtensions = this.#initializeExtensions()
  }

  get lexicalExtensions() {
    return this.enabledExtensions.map(ext => ext.lexicalExtension).filter(Boolean)
  }

  initializeToolbars() {
    if (this.#lexxyToolbar) {
      this.enabledExtensions.forEach(ext => ext.initializeToobar(this.#lexxyToolbar))
    }
  }

  #initializeExtensions() {
    const extensionDefinitions = Lexxy.global.get("extensions")

    return extensionDefinitions.map(
      extension => new extension(this.lexxyElement)
    ).filter(extension => extension.enabled)
  }
}

重要なポイント:

  • 各エディタインスタンスごとに拡張がインスタンス化される
  • enabledfalse を返す拡張は自動的にフィルタリングされる
  • Lexical拡張とツールバー初期化が分離されている

3. エディタへの統合

エディタ要素のライフサイクル内で拡張機能が初期化され、Lexicalエディタに組み込まれます。

connectedCallback() {
  this.id ??= generateDomId("lexxy-editor")
  this.config = new Configuration(this)
  this.extensions = new Extensions(this)
  this.highlighter = new Highlighter(this)

  this.editor = this.#createEditor()
  // ...
}

get #lexicalExtensions() {
  const extensions = [ ]
  const richTextExtensions = [
    this.highlighter.lexicalExtension,
    TrixContentExtension
  ]

  if (this.supportsRichText) {
    extensions.push(...richTextExtensions)
  }

  extensions.push(...this.extensions.lexicalExtensions)

  return extensions
}

4. グローバル設定への登録

拡張機能は、グローバル設定の extensions 配列に登録します。

const global = new Configuration({
  attachmentTagName: "action-text-attachment",
  attachmentContentTypeNamespace: "actiontext",
  authenticatedUploads: false,
  extensions: []
})

使用例:

import * as Lexxy from "@37signals/lexxy"

class MyLexxyExtension extends Lexxy.Extension {
  get enabled() {
    return this.editorElement.supportsRichText
  }

  get lexicalExtension() {
    return defineExtension({
      // Lexical extension definition
    })
  }

  initializeToolbar(lexxyToolbar) {
    // Custom toolbar logic
  }
}

Lexxy.configure({
  global: {
    extensions: [ MyLexxyExtension ]
  },
  default: {
    my_extension: {
      enableCoolFeature: true
    }
  }
})

アーキテクチャ上の利点

モジュール分離の改善

この変更では、コードの整理も行われています:

  • sanitization_helper.js: HTML sanitization機能を独立したモジュールに分離
  • nodes.js: Lexxyのノードクラスをエクスポート可能に
  • code_highlighting_helper.js: highlightAllhighlightCode にリネーム(後方互換性を保持)

npm パッケージのサブモジュールサポート

package.json の exports フィールドが拡張され、ヘルパー関数を個別にインポート可能になりました。

"exports": {
  ".": "./dist/lexxy.esm.js",
  "./helpers": "./dist/lexxy_helpers.esm.js"
}

使用例:

// メインバンドルから分離してヘルパーのみインポート
import { highlightCode } from "@37signals/lexxy/helpers"

Rollupの設定では、manualChunks を使用してヘルパー関数を専用のチャンクに分離しています。

manualChunks: (id) => {
  if (helperChunks.some(file => id.includes(file))) return "lexxy_helpers"
  return null
}

まとめ

この変更により、Lexxyは以下の点で大きく改善されました:

  1. 拡張可能性: プラガブルなExtension APIにより、コア変更なしで機能追加が可能
  2. 柔軟性: enabled getterによる動的な拡張の有効化/無効化
  3. モジュール性: 機能ごとのコード分離と、npmパッケージのサブモジュールサポート
  4. 後方互換性: 既存の highlightAll 関数は deprecated扱いながらもエクスポートを維持

注意点として、PRのノートにもある通り、このAPIは実験的であり、Lexical本体のような完全な依存性管理システムは提供していません。シンプルな拡張メカニズムとして設計されており、今後のフィードバックに基づいて進化する可能性があります。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事構成の3要素(Title, Context, Technical Detail)が明確に記載されています。コードブロック前後の空行やファイル名付きシンタックスハイライト、GitHubリンク記法など、すべてのカスタムMarkdown構文が正しく使用されており、可読性が非常に高いです。内容も専門知識を持つエンジニア向けに適切に調整されています。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

引用されているコードスニペットは、PRの主題である「拡張機能APIの導入」を実装する上で妥当な内容です。`Extensions`クラスのフィルタリングロジック(`.filter(extension => extension.enabled)`)など、コードの重要なポイントと説明が一致しており、技術的に正確です。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRのタイトル「Lexxy extensions」の内容を正確に反映しています。Extension APIの導入という中心的な変更に加え、関連するリファクタリングやnpmパッケージ設定の変更についても言及されており、PRの範囲を網羅的に解説しています。「このAPIは実験的である」というPRの注意点にも触れており、ハルシネーションは見られません。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除