brackets/atomのエクステンションの仕組み

本記事は[Node.js Advent Calendar 2015](http://qiita.com/advent-calendar/2015/nodejs)18日目の記事です。

atomやbracketsなど、WWWの技術で作られたアプリケーションの中にはエクステンションを入れて拡張できるものがいろいろ存在します。
これらのアプリケーションは、どのようにしてエクステンションの仕組みを実現しているのでしょうか。

bracketsとatom(electron)を例にとって調べてみました。

## bracketsはRequireJSベース

bracketsは結論からいうとRequireJSベースのようです。以下詳しく説明します。

### エクステンションの作り方

ざっくりですが、まずはエクステンション自身の作り方を紹介します。

#### main.js

下記は、[wiki](https://github.com/adobe/brackets/wiki/Simple-%22Hello-World%22-extension)に示されているHello World!のサンプルです。

エクステンションは、必ず`main.js`に本体を記述します。

```JavaScript
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window */

/** Simple extension that adds a "File > Hello World" menu item */
define(function (require, exports, module) {
    "use strict";

    var CommandManager = brackets.getModule("command/CommandManager"),
        Menus          = brackets.getModule("command/Menus");


    // Function to run when the menu item is clicked
    function handleHelloWorld() {
        window.alert("Hello, world!");
    }


    // First, register a command - a UI-less object associating an id to a handler
    var MY_COMMAND_ID = "helloworld.sayhello";   // package-style naming to avoid collisions
    CommandManager.register("Hello World", MY_COMMAND_ID, handleHelloWorld);

    // Then create a menu item bound to the command
    // The label of the menu item is the name we gave the command (see above)
    var menu = Menus.getMenu(Menus.AppMenuBar.FILE_MENU);
    menu.addMenuItem(MY_COMMAND_ID);

    // We could also add a key binding at the same time:
    //menu.addMenuItem(MY_COMMAND_ID, "Ctrl-Alt-W");
    // (Note: "Ctrl" is automatically mapped to "Cmd" on Mac)
});
```

これは、ファイルメニューに項目を追加して、それを選択すると「Hello World」のアラートを表示するというものです。

`define`でエクステンションを定義しているようです。
エクステンション本体の関数は`function (require, exports, module)`となっています。
これらの記法はRequireJSの[simplified CommonJS wrapper](https://github.com/jrburke/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#cjs)ですね。

グローバル変数には、`$`, `brackets`, `window`が宣言されているようです。
`require("text!templates/panel.html")`のようにして用意したテンプレートを読み込んだりできます。

#### package.json

エクステンションのメタ情報は、`package.json`に記述します。
下記は`Gist Manager`というエクステンションから持ってきました。

```json
{
    "name": "fezvrasta.gist-manager",
    "title": "Gist Manager",
    "description": "Create and view Github Gists within Brackets. (View > Show Gists Manager)",
    "keywords": ["gist", "GitHub", "snippets"],
    "homepage": "https://github.com/FezVrasta/gist-manager",
    "version": "0.1.1",
    "author": "Fez Vrasta <info@mywebexpression.com> (http://www.mywebexpression.com)",
    "license": "MIT",
    "engines": {
        "brackets": ">=0.24.0"
    }
}
```

### エクステンション読み込み機構

下記のモジュールにエクステンションの読み込み処理が記述されています。

 * [ExtensionLoader.js](https://github.com/adobe/brackets/blob/master/src/utils/ExtensionLoader.js)
 * [ExtensionUtils.js](https://github.com/adobe/brackets/blob/master/src/utils/ExtensionUtils.js)

```JavaScript:ExtensionLoader.js(169):
// Read optional requirejs-config.json
var promise = _mergeConfig(extensionConfig).then(function (mergedConfig) {
		// Create new RequireJS context and load extension entry point
		var extensionRequire = brackets.libRequire.config(mergedConfig),
				extensionRequireDeferred = new $.Deferred();

		contexts[name] = extensionRequire;
		extensionRequire([entryPoint], extensionRequireDeferred.resolve, extensionRequireDeferred.reject);

		return extensionRequireDeferred.promise();
}).then(function (module) {
```

`extensionRequire`でエクステンションのエントリポイント(main.js)を読み込んでいます。
`extensionRequire`の元になっている`brackets.libRequire`は以下のように定義されています：


```JavaScript:Global.js(114):
// Loading extensions requires creating new require.js contexts, which
// requires access to the global 'require' object that always gets hidden
// by the 'require' in the AMD wrapper. We store this in the brackets
// object here so that the ExtensionLoader doesn't have to have access to
// the global object.
global.brackets.libRequire = global.require;
```

つまり、エクステンションの読み込み機構は**require.jsそのものだった**ということですね。

## atom(electron)はNode.jsベース

### ブラウザプロセスでも普通にrequireできる！

Electronではなぜかブラウザプロセス上からでも動的に`require`できます。
例えば以下のコードをグローバルに宣言してみます：

```JavaScript
global.loadModule = function (name) {
  return require(name);
};
```

ElectronアプリケーションのDeveloper Toolsのコンソールで以下のコードを実行すると、正しくモジュールがロードできます。

```JavaScript
loadModule("util"); // -> Object {}
```

という事は、パッケージの読み込みやパッケージ内の依存関係の解決もできますね。


## まとめ

RequireJSって動的なパスでモジュールを読み込めるんですね。
WebpackやBrowserifyのようにあらかじめコンパイルして依存関係を解決しなくてもいいという点が勉強になりました。

atomはnode.jsと同じ感覚で依存関係を記述できる点がすばらしいです。