atomのpackageの作り方

先日、atomというgithub製のIDEが公開されて話題になってます。

f:id:yosuke_furukawa:20140301184804p:plain

これ、広める戦略がうまくて、昔のgmailと同じく、inviteを受けた人が3人だけinvite ticketを持ってて、その人からまた3人inviteできるって仕組みになってます。こうすることでSNSでのinvite ticket要求が盛んになり、流行ってるように見えるというのが上手い。

ちなみにDLされるファイルだけ他人に送っても内部的にチェックしててpackage managerとかが使えない仕組みになってるので、inviteを持ってない人はおとなしく誰かから回ってくるのを待ちましょう。

本題

atom自身はSublime Textっぽい外観で、apmっていうパッケージマネージャが付属されてます。

んで、早速apmに自作のpackageを作って公開してみました。

実行している所:
f:id:yosuke_furukawa:20140302172830g:plain

yosuke-furukawa/language-jsx · GitHub

language-jsx

このlanguage-jsxでは、JSX(DeNA製)のsyntaxハイライト機能、jsxのsnippet機能とPackages > JSX > run から 開いてるjsxファイルを実行できるようにしました。

せっかくなので、作り方と公開方法を説明していきます。

まずはpackageの構造を理解しよう

my-package/
  grammars/       - 文法を定義して、シンタックスハイライトする
  keymaps/           - keybindを定義する
  lib/               - いわゆるライブラリ、大体ココに機能を定義する
  menus/             - メニューの表示を定義する
  spec/                - test用フォルダ
  snippets/          - スニペット、短縮キーワードを登録可能
  stylesheets/      - cssでページのスタイル決めるところ
  package.json      - そのpackageの定義を記述する、npmと同じような書き方

こんな感じのフォルダ構造になります。
npmを作ったことがある人なら分かるかと思いますが、package.jsonというpackageの定義を記述するファイルが必要です。

このフォルダ群の雛形は Packages > Package Generator > Create Atom Package を実行すると簡単なフォルダ群が作成されます。

適当な名前をつけて実行すると、~/.atom/packagesに以下のようなフォルダ群を作ってくれるので楽。

my-package/
  keymaps/
  lib/
  menus/
  spec/
  stylesheets/
  .gitignore
  LICENSE.md
  README.md
  package.json

なので、ここから作業を始めて行くといいと思います。

まずは package.json を確認

package.json

{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "private": true,
  "description": "A short description of your package",
  "activationEvents": ["my-package:toggle"],
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">0.50.0"
  },
  "dependencies": {
  }
}

多分こんな感じの外観になっていると思います。

package.jsonの中身を少し説明しておきます。

この中で重要なのは

  • main (指定必須) entrypointへのパスを指します、これがないと正常にインストールされないので指定必須です。
  • activationEvents (任意指定) packageがロードされてから有効になるイベントです。遅延ロードする時に使います。
  • dependencies (任意指定) ここにnpmのパッケージを指定することができます、今回作成したlanguage-jsxの場合、jsxの依存があるので、以下のようになっています。
 "dependencies": {
    "jsx": "*"
  }

全体としては以下のようになっています。

{
  "name": "language-jsx",
  "version": "0.1.3",
  "main": "./lib/jsx",
  "description": "JSX language support in Atom",
  "engines": {
    "atom": "*",
    "node": "*"
  },
  "activationEvents": [
    "jsx:run"
  ],
  "dependencies": {
    "jsx": "*"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yosuke-furukawa/language-jsx.git"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/yosuke-furukawa/language-jsx/issues"
  }
}

ちなみにsnippetsやstylesheetsのフォルダ名を変えるなら、このpackage.jsonにsnippetsとかstylesheetsとかでキーを指定して、フォルダ名を指定すれば変更できます。

{
  "name": "language-jsx",
  "version": "0.1.3",
  "main": "./lib/jsx",
  "description": "JSX language support in Atom",
  "engines": {
    "atom": "*",
    "node": "*"
  },
  "activationEvents": [
    "jsx:run"
  ],
  "dependencies": {
    "jsx": "*"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yosuke-furukawa/language-jsx.git"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/yosuke-furukawa/language-jsx/issues"
  },
  "snippets" : "./abc",
  "stylesheets": "./css"
}

grammarsを定義しよう

TextMateでもあったと思いますが、Grammarを定義しておくことでsyntaxのハイライトができるようになります。

定義例:grammars/jsx.cson

  {
    'match': '(?<!\\.)\\b(boolean|byte|char|class|double|enum|float|function|int|interface|long|short|void)\\b'
    'name': 'storage.type.jsx'
  }
  {
    'match': '(?<!\\.)\\b(const|export|extends|final|implements|native|private|protected|public|static|synchronized|throws|var)\\b'
    'name': 'storage.modifier.jsx'
  }

こうすることで、varとかclassとかそういうキーワードが修飾語としてハイライトされます。

f:id:yosuke_furukawa:20140302213318p:plain

詳しい定義方法はこちら。
TextMate Manual » Language Grammars

keymapsを定義する

キーマップを定義します。こうすることで、ショートカットキーによるコマンドが定義されます。


定義例:keymaps/jsx.cson

'.workspace':
  'ctrl-r': 'jsx:run'

これで、ctrl+r'jsx:run'コマンドを実行するようになります。

ちなみに、'.workspace'はdivタグに定義されているclassで、全体を表すんですが、特定の状態の時にだけ行いたい場合は、そのクラスを指定するといいです。

'.tree-view-scroller':
  'ctrl-V': 'changer:magic'

これでtree-view-scrollerクラスの上でだけctrl-vが有効になります。

menusを定義する

定義すると画面上部のメニュー一覧から実行できるようになります。

定義例:menus/jsx.cson

'menu': [
  {
    'label': 'Packages'
    'submenu': [
      {
        'label': 'JSX'
        'submenu': [
          {
            'label': 'RUN'
            'command': 'jsx:run'
          }
        ]
      }
    ]
  }
]

これで、Pacakges > JSX > RUN を実行すると、jsx:runが実行されます。

snippetsを定義する

snippetsを定義することで、記述している最中にtabを押すと補完してくれる機能が増えます。

定義例:snippets/jsx.cson

'.source.jsx':
  'class':
    'prefix': 'class'
    'body': 'class ${1:class_name} {\n\n}'
  'main':
    'prefix': 'main'
    'body': 'static function main(args : string[]) :void {\n\t${0:// body...}\n}'

これで、classと打ってからtabを打つとclassの定義が補完されます。
また、mainと打ってからtabを打つと、static function main(args: string[]) :void {}が補完されます。

libを定義する

このlibでパッケージの機能を定義します。今回のlanguage-jsxでは、JSXの実行を行う機能を提供します。

jsx_bin_path = "/node_modules/jsx/bin/jsx"
child_process = require 'child_process'

module.exports =

  # jsx:runで呼び出された時にrunメソッドを実行するようにする
  activate: (state) ->
    atom.workspaceView.command "jsx:run", => @run()

  # ATOMのSHELLをNodeコマンドとして実行するための環境変数をONにする
  # thanks @mootoh
  getExecPath: ->
    "ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 '#{process.execPath}'"

  # 外からnodeコマンドの環境変数を渡せるように
  getNodePath: ->
    atom.config.get("language-jsx.nodepath")

  # 実際のmain処理
  run: ->
    # 現在開いているeditorの本体
    editor = atom.workspace.getActiveEditor()
    # language-jsxのパッケージパス
    lang_jsx_path = atom.packages.resolvePackagePath("language-jsx")
    # jsxの実行コマンドまでのパス
    jsx_bin = lang_jsx_path + jsx_bin_path
    # nodeのコマンドパス
    node_path = @getNodePath() || @getExecPath()

    # getUriで現在開いているファイルのパスを取得する
    uri = editor.getUri()
    # node jsx --run file_name.jsx
    command = "#{node_path} #{jsx_bin} --run #{uri}"
    options = {
      "cwd" : lang_jsx_path
    }

    # 子プロセスから外部コマンド実行する
    child_process.exec(command, options, (error, stdout, stderr) ->
        console.error(error) if error
        console.error(stderr) if stderr
        console.log(stdout) if stdout
    )

    # DevToolを開く
    atom.openDevTools()

こんな感じです、atomAPIはココを見るといいですね。

Atom API Documentation

nodeの外部コマンド呼び出しでハマったこと

jsxのコマンドはnode.jsのスクリプトで出来ているので、nodeから実行することが可能です、ただ、Atomそのものも中身はnode.jsなので、node.jsがmacにインストールされていなくてもAtomさえインストールされていれば実行できるようにしたい所です。

ただ、Atomprocess.execPathを取るとnodeを直接使用しているのではなく、 "Atom Helper"と呼ばれるコードを実行していることが分かります。

これから JSXを呼びだそうとしても、atomが起動してしまい、うまくいきません。

そこでどうするかというと、ATOM_SHELL_INTERNAL_RUN_AS_NODEという環境変数があることが中身のbinaryファイルから分かるので、その環境変数を無理矢理いじるとatomではなく、atomが内部で使っているnodeをnodeとしてそのまま実行することが可能です。

バイナリを解析してくれたのは @mootoh さん、感謝です。

atomのpackageを公開する。

ここだけはコマンドで実行します。

$ cd my-package
$ apm publish

で公開されます。んで、一度公開した後に修正したいときなどは

$ apm publish patch

ってやると勝手にpackage.jsonのversionのpatchバージョンをincrementしてくれます。
minorバージョンアップとmajorバージョンアップもあるので、その辺の詳しい紹介はapm helpを実行して下さい。

$ apm help

まとめ

jsxのatomパッケージを作って公開しました、あとpackageの作り方を紹介しました。

もっと詳細が知りたい場合は公式ドキュメントを閲覧して下さい。

とりあえず作ってみたい場合は2つ目のtutorialから始めるのがオススメです。


Happy atom life!!!