TURBOWARP-PLUS-EDITOR · DEVELOPER GUIDE

TurboWarp-Plus-Editor 開発者ガイド

エディターに機能を足す三つの方式

Scratch のアドオン、TurboWarp のアドオン、TurboWarp-Plus-Editor の差し込み口を比べ、Plus が追加機能を組み込む仕組みの仕様と、新しい追加機能の作り方をまとめた4つの文書です。

この文書では、Plus がエディター本体に組み込んで標準 TurboWarp に足している機能を追加機能、作品が読み込んで使う Scratch・TurboWarp の拡張機能を拡張と呼び分けます。

Scratch Addons(Scratch 向け) TurboWarp 同梱アドオン TurboWarp-Plus-Editor
01 · 短い記事

アドオンは外から触り、Plus は内から呼ばれる

Scratch 系のエディターに機能を足す方法は大きく三つあります。違いの核心は、機能のコードとエディターのどちらが相手を呼ぶかです。

Scratch 3 の公式エディターには、利用者が機能を足すための仕組みがありません。そこを埋めたのが、有志のブラウザー拡張 Scratch Addons です。scratch.mit.edu のページにスクリプトを注入し、画面の要素や内部の状態を外から探して機能を付け足します。

TurboWarp は、この Scratch Addons のうちエディター向けのものを取り込み、自分のビルドに同梱しました。書き方も API もほぼ同じですが、ブラウザー拡張ではなくエディターの一部として動きます。

TurboWarp-Plus-Editor はこの向きを逆にしました。フォークした scratch-gui に「ここで何か渡されていれば使う」という名前付きの口を置き、ビルド時に Plus 側の実装をそこへ結び付けます。何も渡されなければ、フォークは本家の TurboWarp と同じに動きます。

機能のコード エディター アドオン ブラウザー拡張に入っている Scratch 公式エディター scratch.mit.edu ページに注入して、外から探す 要素・Redux・VM を見つけて付け足す 同じビルドの中 同梱アドオン 81件 Scratch Addons から取り込み TurboWarp エディター 本家 scratch-gui 起動後に、同じ API で外から探す Plus の実装 npm パッケージと site/ scratch-gui フォーク 名前付きの口を持つ 決まった口で問い合わせる ビルド時に結合。空なら本家どおり
図1アドオンは機能の側からエディターへ手を伸ばします。Plus ではエディターの側が、口ごとに Plus の実装を呼びます。
Scratch Addons

ブラウザーに入れて使うブラウザー拡張です。Scratch 公式サイトのエディターだけでなく、プロフィールやフォーラムのページにも作用します。

入れ方
利用者がブラウザーに導入
切り替え
ブラウザー拡張の設定画面で個別に
触り方
DOM・Redux・VM を外から
TurboWarp のアドオン

Scratch Addons のエディター向けを同梱したものです。API は TurboWarp が作り直していて、独自の tw- アドオンも 6 件あります。

入れ方
最初から同梱
切り替え
/addons の設定画面
触り方
上と同じ API で外から
TurboWarp-Plus-Editor

フォークの名前付きの口に、ビルド時に実装を渡します。実装の本体はバージョン付きの npm パッケージにあります。

入れ方
ビルドに組み込む
切り替え
起動時の URL フラグ
触り方
GUI から呼ばれる

アドオンの方式は、エディター本体に手を入れずに済みます。そのかわり、画面の構造が変わると黙って効かなくなります。Plus の方式はフォークに呼び出しの行が残りますが、契約が型とテストで固定され、作品の読み込みより前に効かせる処理やレイアウト全体の置き換えまで扱えます。

Plus の主な機能はレイアウト、拡張の審査、保存の流れです。これらは「外から付け足す」モデルに収まらないので、Plus がアドオン互換を目指す意味は大きくありません。外して戻せる小さな機能については、アドオンと同じく後から付けて外せる形で書くことにしています。

詳しい比較は 02 比較と結論、口の一覧は 03 仕様書 にあります。

02 · 比較と結論

三つの方式の比較と、Plus が差し込み口を選ぶ理由

仕組み、効き始める時期、壊れ方、切り替え方の順に比べ、最後に Plus の方式の優位性を理由とともに結論づけます。

1. Scratch Addons:ページに注入されるブラウザー拡張

Scratch Addons は Chrome や Firefox に入れるブラウザー拡張です。アドオンは1つずつマニフェストを持ち、名前・説明・タグ・設定項目・既定で有効かどうか・どのページで動くかを宣言します。条件に合うページを開くと、ブラウザー拡張が userscript と userstyle をそのページに注入します。

userscript は ({addon, console, msg}) を受け取る非同期関数です。addon.tab の waitForElement で画面要素の出現を待ち、traps で VM や Blockly を手に入れ、Redux の状態を見張ります。公式エディターには口がないので、すべてを外から探し当てます。

ブラウザー拡張 マニフェスト+スクリプト ブラウザー拡張の設定 利用者が切り替える 有効・無効、設定値 scratch.mit.edu のページ(公式エディター) userscript ({addon, msg}) 条件に合えば注入 DOM(CSS クラス) Redux ストア VM・Blockly waitForElement 監視・dispatch traps
図2Scratch Addons では、公式エディターは何も知りません。userscript が画面の要素と内部状態を外から見つけて付け足します。

2. TurboWarp のアドオン:同梱され、同じ API で動く

TurboWarp は Scratch Addons のエディター向けアドオンを、pull.js というスクリプトで自分のソースに取り込みます。上流のコードに TurboWarp 用のパッチ(TurboWarp/addons の tw ブランチ)を当て、さらに正規表現で書き換えてから src/addons/ に置きます。今のフォークには 81 件が入っていて、そのうち 6 件は TurboWarp 独自の tw- アドオンです。

実行時は、api.js が Scratch Addons の addon オブジェクトを作り直して渡します。ログイン情報のようにブラウザー拡張にしかない API はありません。そのかわり、appendToSharedSpace の共有スペース(stageHeader、fullscreenStageHeader、afterGreenFlag、afterStopButton、afterSoundTab、monitor、paintEditorZoomControls と、アセットの右クリックメニューの2か所、合わせて 9 か所)のように、TurboWarp の画面に合わせた置き場所が用意されています。メニューバーに「録画」ボタンを足す mediarecorder も、この仕組みでファイルメニューの横の要素を探して差し込みます。

ビルド時 Scratch Addons 上流 TurboWarp/addons tw ブランチ pull.js addons/ generated/ 81 件と一覧 パッチ clone 書き換え 実行時(GUI の起動後) 設定画面 /addons 実行器 entry.js・api.js userscript 切替 addon を渡す 一覧を読む DOM・共有スペース Redux ストア VM・Blockly
図3TurboWarp では、取り込みはビルド時、接続は実行時です。接続のしかたは Scratch Addons と同じく外から探す形です。

3. TurboWarp-Plus-Editor:ビルド時に口へ結び付ける

Plus はフォークした scratch-gui に、本家に無いファイルを1つも置きません。本家にもある src/addons/hooks.js に getHostIntegration() を置き、hooks.js 以外の 28 のファイルが getHostIntegration()?.EditorLayout のように名前を指定して問い合わせます。

配信用のビルドでは、webpack の ProvidePlugin が __TW_GUI_HOST__ を本リポジトリの site/gui-integration/index.ts に結び付けます。そこから site/plus-integration.ts の統合オブジェクトと、npm で版を持つ @kubohiroya/turbowarp-* パッケージへつながります。GUI を単体でビルドすると __TW_GUI_HOST__ は存在せず、getter は null を返し、本家どおりに動きます。

turbowarp-plus-editor(このリポジトリ)と npm パッケージ npm パッケージ @kubohiroya/turbowarp-* 統合オブジェクト site/plus-integration.ts ホスト橋渡し site/gui-integration/ import import webpack ProvidePlugin __TW_GUI_HOST__ 結び付ける scratch-gui フォーク(SHA で固定。本家に無いファイルは 0) 28 ファイルの呼び出し箇所 getHostIntegration()?.X addons/hooks.js 無ければ null 値を供給 名前で問い合わせる
図4Plus ではフォークの呼び出し箇所が口の名前を指定し、ビルド時に供給された値を受け取ります。値が無いときは本家と同じ動きに落ちます。

4. 効き始める時期

三つの方式は、機能がいつ効き始めるかが違います。アドオンはページや GUI が動き出してから非同期に付くので、作品の読み込みより前に必ず効いていなければならない処理には向きません。Plus は、拡張の審査のように読み込みの前に決まっていなければならない処理を、口として同期的に渡せます。

ビルド ページ読み込み GUI 描画 作品の読み込み 編集中 Scratch Addons TurboWarp アドオン Plus 注入され、要素が現れるのを待って付く 利用者が切替 同梱 起動後に非同期で実行 利用者が切替 ビルド時に結合し、描画中に同期的に参照 審査は読み込みの前に効く
図5アドオンは後から付き、利用者が切り替えられます。Plus は最初から結び付いていて、作品の読み込みより前に効かせる処理も持てます。

5. 本家が変わったときの壊れ方

アドオンは CSS モジュールのクラス名を前方一致で探します(scratchClass、[class*='menu-bar_file-group'] など)。本家が画面の構造を変えると、探し当てられずに何も起きません。Plus の口はフォークのソースに書かれた呼び出しなので、本家の変更とぶつかればマージの衝突や型検査、統合テストで止まります。

アドオン 本家がメニューバーの 構造を変える セレクターが 一致しなくなる ボタンが出ないだけ エラーも警告も出ない Plus 本家が menu-bar.jsx を変える マージで呼び出し行と 衝突する 取り込む前に止まる 型検査・統合テスト・E2E
図6アドオンは黙って効かなくなります。Plus はフォークを更新する時点で気付けます。

6. 観点ごとの比較

観点Scratch AddonsTurboWarp のアドオンTurboWarp-Plus-Editor
配布の形ブラウザー拡張エディターのビルドに同梱npm パッケージをビルドに組み込む
機能の単位マニフェスト+userscript/userstyle同じ(_manifest_entry.js に変換)パッケージの関数・コンポーネントと、統合オブジェクトの項目
エディターの改変不要実行器だけ(src/addons/)フォーク 28 ファイルに呼び出し行
接続の時期実行時(注入)実行時(GUI の起動後)ビルド時に結合、描画時に同期参照
接続点DOM・Redux・VM・Blockly を外から同じ+共有スペース 9 か所名前付きの口(React コンポーネント、reducer の断片、サービス)
できること付け足し、部分的な差し替え同じ付け足しに加え、レイアウト全体の置き換え、読み込み前の審査、保存の流れ
切り替え利用者が設定画面で利用者が /addons で起動時の URL フラグ(?twPlus…=1)
本家が変わったとき黙って効かなくなる同じマージ・型検査・テストで止まる
契約の明示開発者ドキュメント文書なし(実装が仕様)型付きの口と単体テスト
他の作品・プレイヤーとの共有なしなし同じパッケージを Packager の単一 HTML でも使う

7. Plus の追加機能をアドオンの形に寄せられるか

Plus の追加機能を「付け足すか、置き換えるか」と「利用者が切ってよいか、常に効いている必要があるか」の二つの軸で並べると、アドオンの形に寄せられるのは左上の区画だけです。

付け足す ←   → 置き換える 利用者が切ってよい 常に効いている A アドオンの形に寄せられる B 見た目と切替だけ寄せられる C 寄せない(口のまま) 右クリックメニューのアイコン ワークスペースのミニマップ パレットの削除プレビュー スクリプト転送のメニュー 作品全体の検索 ステージの操作欄 横長のブロックパレット 別ウィンドウのコード表示 スプライト一覧の強化 起動時に決まる。「リロードが必要」扱いなら揃う 拡張の審査と信頼の一覧 作品状態・モーダルの reducer Packager の行き先 Dockview によるレイアウト全体 ステージパネル・スプライト一覧 ファイルメニューと保存の流れ 読み込み前に効く・切られると困る・構造を置き換える
図7アドオンの形に寄せられる A は付け足しの小さな機能だけです。Plus の中心にある C は、アドオンのモデルの外側にあります。

8. 結論:Plus の方式が優れている理由

TurboWarp-Plus-Editor が TurboWarp に機能を足すには、アドオンの方式よりも、フォークに名前付きの口を置いてビルド時に実装を結び付ける今の方式が適しています。理由は次の6つです。

  1. 作品より先に効かせられる拡張の審査や信頼する拡張の一覧は、作品の読み込みより前に決まっていなければなりません。起動後に非同期で付くアドオンでは、この順序を保証できません。口は描画の時点で同期的に参照されます。
  2. 構造ごと置き換えられるDockview のレイアウト、ステージパネル、スプライト一覧、保存の流れは、既存の画面に付け足すものではありません。アドオンで実現すると、本家の DOM を隠して作り直すことになります。口なら React コンポーネントや reducer の断片をそのまま渡せます。
  3. 契約が見え、テストできるアドオンの接続点はハッシュ付きのクラス名や画面の文言です。Plus の口は名前と引数が決まっていて、test/plus-integration.test.ts や test:gui-integration で固定できます。本家が変わったときも、黙って壊れずにテストで止まります。
  4. 本家との差分を小さく保てる口はすべて任意で、何も渡さなければ本家と同じに動きます。フォークに残るのは呼び出しの行だけで、本家に無いファイルは 0 件です。本家の更新を取り込むときの衝突が、呼び出しの行に限られます。
  5. 実装を版付きの部品として再利用できる実装の本体は @kubohiroya/turbowarp-* の npm パッケージにあり、それぞれが版・テスト・リリースを持ちます。同じパッケージを Packager が作る単一 HTML のプレイヤーでも使えます。アドオンはそのエディターの中でしか動きません。
  6. 実行時の拡張ともつながる口の一部は Scratch.gui.stageControls や Scratch.gui.blockEditors として拡張に公開され、カメラなどの拡張が Editor の操作欄に自分の操作を登録できます。アドオンの API は拡張から使う前提になっていません。
引き受けているコスト

利用者が機能を個別に切り替える画面はありません。フォークの呼び出し行を本家の更新のたびに保守する必要があり、機能を足すにはビルドし直す必要があります。第三者が実行時に機能を足すこともできません。ただし、エディター本体ではない機能は、実行時の拡張として操作欄の API から足せます。

そのうえで、図7の A に当たる小さな機能は、アドオンと同じく「後から付けて、dispose で外せる」形で書きます。こうしておけば、利用者が切り替えられるようにしたくなったとき、アドオンの形へ移しやすくなります。既存の機能をアドオン互換に作り直す作業は行いません。

根拠:フォーク src/addons/(81 件、うち tw- 6 件)、api.js の appendToSharedSpace、docs/plus-integration-sockets.md、docs/gui-integration/README.md、site/plus-integration.ts。件数は 2026年11月14日に、このリポジトリが固定しているフォーク(site/source.json の guiRevision、f1e8f96)で数えたものです。フォークを更新したら数え直します。

03 · 仕様書

TurboWarp-Plus-Editor が追加機能を組み込む仕組み

Plus の追加機能がフォークの scratch-gui に届くまでの経路、口の一覧、守るべき約束、起動時フラグ、実行時の拡張に公開する API を定めます。

1. 3つのリポジトリと依存の向き

Plus の追加機能は3つのリポジトリにまたがります。依存は一方向で、上流は下流を知りません。

turbowarp-plus-packages npm・MPL-2.0 など ・実装の本体 ・単体テスト ・版とリリース(v タグ) ・下流を知らない turbowarp-plus-editor GPL・このリポジトリ ・site/ の接続コード ・config/ の起動時フラグ ・ビルド設定と配信 ・統合テストと E2E scratch-gui フォーク GPL・SHA で固定 ・本家にあるファイルだけ ・口を呼ぶ行 ・汎用の小さな修正 ・独自コードを足さない npm 公開版を import 固定して値を渡す / 口で問い合わせる GitHub Pages・player・Packager
図8実装はパッケージに、接続と配信は Editor に、口を呼ぶ行だけがフォークにあります。パッケージ側は GPL のコードを import しないので、向きは「GPL 側がパッケージの口を呼ぶ」に固定します。

2. ビルド時の結合

フォークの src/addons/hooks.js は次の getter を持ちます。本家にも存在するファイルです。

/* global __TW_GUI_HOST__ */
// The distributing build supplies this module; standalone GUI builds have no extra host.
export const getHostIntegration = () =>
    (typeof __TW_GUI_HOST__ === 'undefined' ? null : __TW_GUI_HOST__);

配信ビルドの scripts/site/webpack.config.cts が ProvidePlugin で __TW_GUI_HOST__ を site/gui-integration/index.ts に結び付けます。実行時の global への代入や起動順には依存しません。フォークが読む値は2層になっています。

フォークの呼び出し箇所 ?.EditorLayout ?.renderFileMenu(props) ?.hostModalNames( ?.hostIntegration()) site/gui-integration/index.ts 直接の値 コンポーネント・関数 host* 補助関数 tw-plus-integration-points.ts site/plus-integration.ts 統合オブジェクト modals translations attachToWorkspace stageControls securityReview … plusIntegration() hostIntegration() どちらも無ければ null か空の既定値([]、{}、null)を返し、フォークは本家の動きに落ちる
図9画面の部品は橋渡しから直接読み、設定値やサービスは補助関数を通して統合オブジェクトから読みます。補助関数が「無いときの既定値」を一か所で決めます。

3. 口の一覧

フォークが参照している口を用途ごとに並べます。左の名前はフォークが getHostIntegration()?. の後に書く名前、右の項目は統合オブジェクトの項目です。menu-bar.jsx と extension-library.jsx は getHostIntegration() をいったん変数 host に受けてから読むので、最新の一覧はフォークの checkout で次の2つのコマンドを合わせて取り出します。hostIntegration は口ではなく、host… 補助関数に統合オブジェクトを渡すための getter です。

表は 2026年11月14日に、固定しているフォーク(f1e8f96)で取り出したものです。

grep -rhoE "getHostIntegration\(\)\?\.[A-Za-z]+" src | sort | uniq -c
grep -hoE "host\??\.[A-Za-z]+" src/components/menu-bar/menu-bar.jsx src/containers/extension-library.jsx | sort -u
用途フォークが読む名前統合オブジェクトの項目主な呼び出し元
画面の構造EditorLayout StagePanel StageDisplayMenu TargetListLayout TargetListPanel SpriteGrid BackdropTile BackdropListActions BackpackSurface(直接の値)gui.jsx ほか
スプライトとステージwrapSpriteInfo spriteListClasses useSpriteCodeWindows attachSpriteCodeWorkspace attachStageInput attachStageMonitorDrag(直接の値)sprite-info.jsx、stage.jsx ほか
メニューrenderFileMenu createFileMenuItem extensionInFileMenu renderProjectSearch ProjectInfoMenu renderStageControls(直接の値)menu-bar.jsx、controls.jsx
ワークスペースとブロックcreateWorkspaceAttachment registerHostBlockShapes createBlocksWrapper getBlockEditorService attachHostScriptTransferattachToWorkspace registerBlockShapes blockEditor attachWorkspaceMinimapgui.jsx、blocks.js、blocks.jsx
状態hostModalNames hostModalDefaults createCancelSecurityLoad recoverProjectLoad loadedProjectTitle restoreProjectTitlemodalsreducers/、vm-manager-hoc.jsx
拡張の読み込みと審査installSecurityHost getPlusReviewedMode createPreviewVm hostLibraryTabinstallExtensionHost securityReview trustedExtensions libraryTab extensionLoadErrorstw-security-manager.jsx、extension-library.jsx
入力と翻訳hostIgnoresKeyEvent hostTranslationsForshouldIgnoreKeyEvent translationsvm-listener-hoc.jsx、tw-translations/
アドオンとの接点hostAwaitedAddonIds finalizeHostTransferMenu hostStageTransferMenuawaitStartupAddons scriptTransferaddons/api.js
PackagerhostProjectPackagerprojectPackagertw-packager-integration-hoc.jsx
拡張に公開する APIhostStageControls getBlockEditorService createPreviewVmstageControls blockEditortw-extension-gui-api.js

4. 口の約束

すべての口は次の約束を守ります。新しい口を開けるときも同じです。

  1. 任意であること。値が無ければ、フォークは本家の TurboWarp と同じに動きます。補助関数は空の既定値([]、{}、null)を返します。
  2. GUI 内部の値は引数で受け取ること。ホストはフォークの reducer や UI を import しません。Modal、遅延読み込みの ScratchBlocks、logger、Redux の action creator は、フォークが呼び出し時に渡します(例:createCancelSecurityLoad(initialState, defaultProjectId, LoadingState, integration))。
  3. 付けたものは外せること。何かを付ける口は {dispose()} を返します。フォークは VM が替わるたびに前の戻り値を dispose() してから呼び直し、戻り値の中は見ません。
  4. VM ごとに同じサービスを返すこと。操作欄のように二か所から読まれるサービスは、VM をキーにした WeakMap で1つに保ちます。
  5. CSS の口は作らないこと。本家の CSS モジュールのクラス名はビルド時にハッシュされ、外から狙えません。新しい部品は自分のクラスとスタイルをパッケージ側に持ち、色は GUI の公開テーマ変数を使います。
  6. 独自のファイルをフォークに足さないこと。フォークに残すのは口を呼ぶ行だけです。テストもフォークには足さず、このリポジトリの test/ とパッケージ側に置きます。

5. ワークスペースへの取り付け

attachToWorkspace は、Blockly や VM に付く機能をまとめて取り付ける口です。アドオンの userscript に最も近い形をしています。

フォーク(gui.jsx) 統合オブジェクト パッケージ attachToWorkspace(context) 取り付け関数を順に呼ぶ {dispose} をそれぞれ返す まとめた {dispose} VM が替わる dispose() 逆順に外す。失敗しても残りを外す attachToWorkspace(新しい context)
図10取り付けと取り外しの順序はフォークが持ち、何を付けるかは統合オブジェクトが決めます。
context の項目内容
vm今の VM。替わるたびに呼び直される
locale表示言語(ja、en など)
documentエディターの document。別ウィンドウにも対応するため window.document を直接使わない
getBlocklyScratchBlocks を返す関数。読み込まれるまで待つ
getMessages今の言語の翻訳を返す関数

ScratchBlocks が使えるようになった時点で一度だけ行う登録(ブロックの形、右クリックメニュー、パレットへの項目の追加)は、registerBlockShapes(ScratchBlocks) に置きます。アドオンの起動を待つ必要があるときは、そのアドオンの id を awaitStartupAddons に書きます(今は editor-searchable-dropdowns)。

6. 実行時の拡張に公開する API

非サンドボックスの拡張は Scratch.gui から次の値を読めます。本家にある getBlockly と getBlocklyEagerly に加え、ホストが値を渡したときだけ次の項目が生えます。値が無いときは undefined のままなので、拡張は「このビルドには無い」と判断できます。

項目内容
Scratch.gui.stageControlsステージの操作欄にボタンを足す API。addButton({id, label, text?, icon?, onClick}) が {update, focus, dispose} を返す
Scratch.gui.blockEditorsフォーム形式のブロック編集サービス
Scratch.gui.createPreviewVm()作品とは独立したプレビュー用の VM を作る
非サンドボックス拡張 作品が読み込む app-shell を使う拡張 camera-source など controls dock installControlsDock Symbol.for(…/v1) 操作欄サービス VM ごとに1つ WeakMap Scratch.gui.stageControls.addButton() service.api メニューバー末尾 renderStageControls 購読 PROJECT_LOADED で空にする
図11どちらの経路から登録しても同じサービスに入り、Editor のメニューバーに並びます。Editor 自身は個々の拡張を知りません。

addButton で足したボタンは visible: false で始まります。表示するには update({visible: true}) を呼びます。update で変えられるのは label、text、visible、expanded の4つです。展開したときに見せる要素は、サービスの placePanel(element) でホストに渡します。要素の持ち主は登録した側で、ホストは取り付けるだけです。

7. 起動時フラグ

機能の有効・無効は、ページの URL クエリ twPlus… で起動時に一度だけ決まります。config/web-startup-feature-flags.ts が URL を読み、config/feature-flags.ts の readStartupFeatureFlags が凍結したオブジェクトを返します。作品のデータからこの値を受け取ってはいけません。

?twPlusXxx=1 ページの URL readWebStartup FeatureFlags readStartup 凍結して返す 何を渡すか決める site/* 作品データ(SB3)からは読まない
図12フラグは起動時に URL から一度だけ読み、以後は変わりません。
既定フラグ(twPlus を省略)
有効(=0 で無効)ExtensionLoader ProjectTools PayloadFormats Splitter StandardLayout(Web の既定) PaletteDeletePreview PaletteCategoryReorder ContextMenuIcons LocalGit WorkspaceMinimap StageCodePrewarm
無効(=1 で有効)SpriteProperties WindowLayout HorizontalBlockPalette StageDisplayMenu(StandardLayout のときは有効)
他のフラグが前提(既定で有効)ManualProjectURI(ExtensionLoader が必要)、ExtensionFileMenu(StandardLayout が必要)
他のフラグが前提(=1 で有効)GitRestoreVersion(LocalGit が必要)、BackpackPopout InternalWindows(WindowLayout が必要)、StagePanel TargetList SpriteCodeWindows(WindowLayout と InternalWindows が必要)

8. 同梱アドオンとの関係

  • 既定の有効化:site/addon-manifests.ts が、webpack の alias でフォークの generated/addon-manifests を置き換えます。「おすすめ」タグの付いたアドオンと hide-delete-button、block-palette-icons を既定で有効にし、hide-flyout は無効のままにします。利用者が設定画面で選んだ値が優先されます。
  • 起動待ち:awaitStartupAddons に書いたアドオンは、実行器がその起動を待ってから次へ進みます。
  • 右クリックメニュー:api.js の createBlockContextMenu は、項目を確定する前に finalizeHostTransferMenu を通します。スクリプト転送のメニューはここで足されます。

9. 検証

コマンド確かめること
npm testこのリポジトリと packages/ の単体テスト。test/plus-integration.test.ts が統合オブジェクトの各項目を固定する
E2E_GUI_ROOT=… npm run test:gui-integration固定したフォークに本家に無いファイルが無いこと、口の意味、移設したテスト
npm run typecheck型検査
E2E_GUI_ROOT=… npm run build:pages配信ビルド。build-info.json の revision と siteRevision が今の HEAD と site/source.json に一致すること
npm run test:e2ePlaywright による統合 E2E
E2E_GUI_ROOT

固定したフォークを checkout したディレクトリの絶対パスを指定します。CI と同じく .host/scratch-gui に置くのが基本です。checkout の HEAD は site/source.json の guiRevision と一致している必要があり、食い違うと test:gui-integration は両方の revision を示して止まります。

04 · 作り方

新しい追加機能を作る手順

作りたい追加機能をどこに置くかを決め、パッケージに実装し、Editor でつなぎ、検証して配信するまでの手順です。

1. 置き場所を決める

最初に、機能がどの経路でエディターに届くかを決めます。下の図の上から順に当てはめ、最初に「はい」になったところが置き場所です。

作りたい機能 作品の中で動く ブロック・入出力? いいえ 既存の口で 届く? いいえ Blockly・VM に付けて 外せる? いいえ D 新しい口を開ける フォークに呼び出し1行+受け側の補助関数 A 実行時の拡張として作る 操作欄は Scratch.gui.stageControls か app-shell はい B パッケージに実装し、site/ でつなぐ フォークは変えない はい C attachToWorkspace などの中で付ける {dispose} を返す。フォークは変えない はい
図13A〜C はフォークを変えずに済みます。D だけがフォークへの変更を伴うので、先に A〜C で届かないことを確かめます。

2. 作業の順序

実装はパッケージのリポジトリで先に公開し、Editor はその npm 公開版に上げてからつなぎます。新しい口が要るときは、フォークの変更を並行して進め、Editor で SHA を固定します。

packages フォーク (D のみ) plus-editor ① 実装と 単体テスト ② bump で版を決め v タグで npm 公開 ①' 既存ファイルに口を呼ぶ行を足す PR 独自ファイル・テストは足さない ③ 版と SHA を上げる ④ site/ で つなぐ ⑤ 検証 して PR npm 公開版 source.json の SHA
図14上流から下流へ一方向に進みます。Editor の変更は、公開された版を使うところから始まります。

3. 手順

  1. 置き場所と口を決める

    図13で A〜D のどれに当たるかを決め、使う口を仕様書の口の一覧から選びます。実装がどのパッケージに入るかもこの時点で決めます。Editor の packages/ にある既存の実装は移行途中のもので、新しい機能の置き場所にはしません。

  2. パッケージに実装する

    turbowarp-plus-packages の該当パッケージ(無ければ新しいパッケージ)に実装します。GPL のコードは import せず、エディターから受け取るものはすべて引数でもらいます。付ける機能は {dispose} を返します。

    // パッケージ側:document と翻訳を受け取り、外す手段を返す
    export const attachSpriteBadges = (hostDocument, {vm, getMessages}) => {
        const onTargetsUpdate = () => render(hostDocument, vm, getMessages());
        vm.on('targetsUpdate', onTargetsUpdate);
        return {
            dispose () {
                vm.removeListener('targetsUpdate', onTargetsUpdate);
                clear(hostDocument);
            }
        };
    };

    スタイルはパッケージの中に自分のクラスで持ち、色は GUI の公開テーマ変数を使います。単体テストもパッケージに置きます。

  3. 版を上げて npm に公開する

    turbowarp-plus-packages の npm run bump(上流から)または npm run bump:downstream(下流へ伝える)で版を決め、v<版> のタグで npm に公開します。tgz を Editor に同梱する方法は使いません。

  4. (D のときだけ)フォークに口を開ける

    本家にもあるファイルの中に、口を呼ぶ行を足します。値が無いときは本家どおりに動くように書きます。

    // scratch-gui フォーク:既存ファイルに足すのはこの形の行だけ
    const SpriteBadges = getHostIntegration()?.SpriteBadges || null;
    // ...
    {SpriteBadges ? <SpriteBadges vm={vm} /> : null}

    新しいファイルやテストはフォークに足しません。口の意味は Editor 側のテストで押さえます。統合オブジェクトの項目として読ませる場合は、site/gui-integration/lib/tw-plus-integration-points.ts に既定値を返す host… 補助関数を足します。

  5. Editor で版と SHA を上げる

    package.json と package-lock.json を npm 公開版に上げます。D のときは site/source.json のフォークの SHA を、口を開けたコミットの40桁 SHA に替えます。

  6. site/ でつなぐ

    C の場合は site/plus-integration.ts の attachToWorkspace に1行足します。

    attached.push(attachSpriteBadges(hostDocument, {vm, getMessages}));

    B と D で画面の部品を渡す場合は、site/gui-integration/index.ts から export します。起動時フラグで切り替えたい場合は、config/feature-flags.ts と config/web-startup-feature-flags.ts に twPlus… を1つずつ足し、フラグが無効のときは何も渡さないようにします。

    // site/gui-integration/index.ts
    export const SpriteBadges = startup.spriteBadges ? SpriteBadgesView : undefined;
  7. A の場合:拡張から操作欄に登録する

    実行時の拡張は、Editor の内部を知らずに操作欄を使えます。Scratch.gui.stageControls が無いビルドでは何もしないようにします。

    const controls = Scratch.gui?.stageControls;
    if (controls) {
        const button = controls.addButton({
            id: 'my-camera.calibrate', label: 'カメラを較正する', icon: '◎',
            onClick: () => startCalibration()
        });
        button.update({visible: true}); // 追加直後は非表示
    }

    パッケージとして配る拡張は、@kubohiroya/turbowarp-app-shell の操作欄を使うと、ツールバーの無い単一 HTML のプレイヤーでも同じ操作が出ます。

  8. 検証する

    npm test、npm run typecheck、npm run test:gui-integration を通し、E2E_GUI_ROOT=… npm run build:pages でビルドし直してから画面を確認します(E2E_GUI_ROOT は仕様書の検証を参照)。scratch-gui/build に残っている古い成果物は使いません。build-info.json の revision と siteRevision が今の状態に一致しているか確かめます。

  9. PR を出す

    変更の内容と確かめたことを書いて PR を出し、CI の結果を見てからマージします。

4. 画面部品の約束

追加機能が出すダイアログやパネルは、次の形に揃えます。

  • 下部の操作ボタン:右端にまとめ、キャンセルを左、保存・適用などの主操作を右に置きます。共通の tw-plus-dialog-actions を使い、ボタンの data-dialog-action に cancel、close、confirm、delete のいずれかを指定します。文字のラベルを残し、キャンセルと閉じるには×、保存・確定にはチェック、削除にはごみ箱のアイコンを添えます。
  • 見出しの横の閉じるボタン:パネルやダイアログの右上に置く閉じるボタンには「閉じる」「Close」の文字を出さず、×アイコンだけにします。名前は翻訳した title と aria-label で示します。
  • 文字列:表示する文字列は ja と en の翻訳を持ちます(ひらがな表示が要るものは ja-hira も)。

5. 出す前の確認

  • 実装の本体はパッケージにあり、Editor の変更は版の更新と数行の接続に収まっている
  • 付けたものはすべて dispose() で外れる。VM が替わっても二重に付かない
  • 値を渡さないとき、フォークは本家の TurboWarp と同じに動く
  • GUI 内部の値は引数で受け取り、フォークのモジュールを import していない
  • 表示する文字列は ja と en の翻訳を持つ(ひらがな表示が要るものは ja-hira も)
  • 起動時フラグは URL からだけ読み、作品のデータから読んでいない
  • 単体テストはパッケージに、口のテストは Editor の test/ にある
  • ビルドし直した成果物で画面を確かめた

してはいけないこと

  • フォークに新しいファイル、独自のテスト、本家のクラス名を狙う CSS を足す
  • 本家の DOM をクラス名で探して書き換える(それはアドオンの方式で、口があるなら口を使う)
  • パッケージから GPL のコード(フォークや Editor の site/)を import する
  • パッケージを tgz で Editor に同梱する

6. このリポジトリの保守者向けの注意

保守者・AI エージェント向け

ここからは、turbowarp-plus-editor に直接変更を入れる人(AI エージェントを含む)向けの運用規則です。正本は AGENTS.md です。

  • PR に --auto を付けない。このリポジトリの PR には必須のチェックが無く、--auto を付けると CI を待たずにすぐマージされる
  • OS のマウスカーソルを動かす E2E(TW_PLUS_NATIVE_INPUT=1、test:e2e:native)を、明示的に依頼されていないのに走らせない。一般的な E2E の実行や PR の検証の依頼には含まれない

関連文書:docs/plus-integration-sockets.md(口の設計の経緯)、docs/gui-integration/README.md(移設とビルド時の接続)、docs/architecture.md(責務と配信)。