atomのpackageの作り方
先日、atomというgithub製のIDEが公開されて話題になってます。
これ、広める戦略がうまくて、昔のgmailと同じく、inviteを受けた人が3人だけinvite ticketを持ってて、その人からまた3人inviteできるって仕組みになってます。こうすることでSNSでのinvite ticket要求が盛んになり、流行ってるように見えるというのが上手い。
ちなみにDLされるファイルだけ他人に送っても内部的にチェックしててpackage managerとかが使えない仕組みになってるので、inviteを持ってない人はおとなしく誰かから回ってくるのを待ちましょう。
本題
atom自身はSublime Textっぽい外観で、apmっていうパッケージマネージャが付属されてます。
んで、早速apmに自作のpackageを作って公開してみました。
実行している所:
yosuke-furukawa/language-jsx · GitHub
この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
とかそういうキーワードが修飾語としてハイライトされます。
詳しい定義方法はこちら。
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()
nodeの外部コマンド呼び出しでハマったこと
jsxのコマンドはnode.jsのスクリプトで出来ているので、nodeから実行することが可能です、ただ、Atomそのものも中身はnode.jsなので、node.jsがmacにインストールされていなくてもAtomさえインストールされていれば実行できるようにしたい所です。
ただ、Atomのprocess.execPath
を取るとnodeを直接使用しているのではなく、 "Atom Helper"
と呼ばれるコードを実行していることが分かります。
これから JSXを呼びだそうとしても、atomが起動してしまい、うまくいきません。
そこでどうするかというと、ATOM_SHELL_INTERNAL_RUN_AS_NODE
という環境変数があることが中身のbinaryファイルから分かるので、その環境変数を無理矢理いじるとatomではなく、atomが内部で使っているnodeをnodeとしてそのまま実行することが可能です。
Atom バイナリを Node として CLI から起動する方法: `ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 Atom.app/Contents/MacOS/Atom` > @yosuke_furukawa
— Motohiro Takayama (@mootoh) February 28, 2014
バイナリを解析してくれたのは @mootoh さん、感謝です。
atomのpackageを公開する。
ここだけはコマンドで実行します。
$ cd my-package $ apm publish
で公開されます。んで、一度公開した後に修正したいときなどは
$ apm publish patch
ってやると勝手にpackage.jsonのversionのpatchバージョンをincrementしてくれます。
minorバージョンアップとmajorバージョンアップもあるので、その辺の詳しい紹介はapm helpを実行して下さい。
$ apm help