dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Vim]SyntasticによるESLintチェックが遅いのでNeomakeに乗り換えた

(2017/01/23追記)

この後 Flow を導入しようとしたら色々問題が発生したので、Neomake から ALE に乗り換えた。

(追記ここまで)


(2018/04/15追記)

現在、記事を書いた時と設定方法が変わっているようです。
こちらの方が最新の手順をまとめてくださっているので、ご参照ください。

(追記ここまで)


Vim の Syntax Checker として有名なのは Syntastic ですね。
最近は JavaScript を書くことが多いので、この Syntastic を使って ESLint のチェックをできるようにしていました。

上の記事に書いてある設定を行ったことで、Vim で常に lint チェックをかけられるようになったのは良かったんですが
1 個だけ不満があって、チェックのたびに操作がブロックされてしまうという問題がありました。

f:id:dackdive:20161023121542g:plain 正直こればっかりは我慢するしかないかなーと思ってたんですが、Neomake という Syntastic に替わるプラグインの存在を知ったので試してみたところ、かなり快適だったので紹介します。

なお、Vim でも NeoVim でもどっちでも動きました。
(動作確認した Vim のバージョンは 8.0.22)


インストール

プラグイン管理ツールを使っている場合、他のプラグインと同じようにインストールできます。
以下は、dein.vim を使っている場合の書き方です。

call dein#add('neomake/neomake')
autocmd! BufWritePost * Neomake " 保存時に実行する
let g:neomake_javascript_enabled_makers = ['eslint']

call dein#add('benjie/neomake-local-eslint.vim')

let g:neomake_error_sign = {'text': '>>', 'texthl': 'Error'}
let g:neomake_warning_sign = {'text': '>>',  'texthl': 'Todo'}

単純に動かすだけなら最初の3行だけでいいです。
残りの設定については以下の通り。


ESLint をグローバルインストールせずに使えるようにする

Syntastic でもやったアレ。プロジェクトごとにローカルインストールした ESLint だけで動くようにしたいものです。
同様のプラグインがありました。
https://github.com/benjie/neomake-local-eslint.vim

Neomake 本体と同様、インストールするだけで OK。

call dein#add('benjie/neomake-local-eslint.vim')


エラー行に表示されるアイコンをカスタマイズする

デフォルトで、エラーや警告のある行に表示されるアイコンはこんな感じ。

f:id:dackdive:20161023113225p:plain

カラースキームのせいもありますがあんまり目立ちませんね。
自分好みにカスタマイズしてみます。

:h naomake

でヘルプドキュメントを読むと、g:neomake_error_signg:neomake_warning_sign に設定するそうです。
また、デフォルトの設定は以下らしい。

let g:neomake_error_sign = {'text': '✖', 'texthl': 'NeomakeErrorSign'}
let g:neomake_warning_sign = {
     "\   'text': '⚠',
     "\   'texthl': 'NeomakeWarningSign',
     "\ }
let g:neomake_message_sign = {
     "\   'text': '➤',
     "\   'texthl': 'NeomakeMessageSign',
     "\ }

ESLint では error と warning を使ってるっぽいので、こうします。

let g:neomake_error_sign = {'text': '>>', 'texthl': 'Error'}
let g:neomake_warning_sign = {'text': '>>',  'texthl': 'Todo'}

text がカーソル行に表示される文字列で、どちらも Syntastic のときと同じ >> にしています。
texthlVim で定義済みのハイライトグループ(?) からグループ名を指定します。

このあたりはあんま詳しくないので、

:highlight

でグループ一覧を開き、Syntastic のときと同じ見た目になりそうな ErrorTodo を選びました。

f:id:dackdive:20161023114002p:plain

設定後の見た目。エラー行見やすくなった。

f:id:dackdive:20161023114101p:plain


使う

f:id:dackdive:20161023121728g:plain

伝わりづらいけど、保存時に lint チェックを実行しても編集処理がブロックされなくなりました。

また、Syntastic と同じく、エラー行にカーソルを持っていくと下部にエラー内容が表示されるだけでなく、

:lopen

でエラー一覧を location list で確認することもできます。(location list 内で Enter キーを押すと該当エラー行にジャンプできます)

f:id:dackdive:20161023122004p:plain

Reactでフォームの項目をどう扱うか問題

メモ。

なんの話か

  • React でフォーム項目を簡潔に書く方法がわからない
  • 管理されたコンポーネントで書こうとすると、項目の数だけ state と対応するイベントハンドラが必要になる
    • 親に渡す必要がある、とかだとさらにしんどい
// 基本形
class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };

    this.onChangeName = this.onChangeName.bind(this);
  }

  onChangeName(e) {
    console.log(e.target.value);
    this.setState({ name: e.target.value });
  }

  render() {
    return (
      <form>
        <input type="text" value={this.state.name} onChange={this.onChangeName} />
      </form>
    );
  }
}


どうすべきか

いくつか方法はある。
1 と 2 は微妙に違うが、「イベントハンドラを1つにまとめる」という戦略自体は同じ。

1. onChange イベントハンドラに引数で key を渡す
export default class MyFormCmp extends React.Component {

  constructor(props) {
    this.state = {
      name: '',
    };

    this.onChangeField = this.onChangeField.bind(this);
  }

  onChangeField(e, key) {
    console.log(key, e.target.value);
    this.setState({ [key]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          type="text" value={this.state.name} onChange={(e) => this.onChangeField(e, 'name')} />
      </form>
    );
  }
}


2. イベントハンドラ内で event.target.name を参照する

参考:ES2015 以降で React 書くなら form 部品での onChange で setState するのもう全部これでいいんじゃないかなあ - BattleProgrammerShibata

export default class MyFormCmp extends React.Component {
  // 略

  onChangeField(e) {
    console.log(e.target.name, e.target.value);
    this.setState({ [e.target.name]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          name="name"
          type="text"
          value={this.state.name}
          onChange={this.onChangeField}
        />
      </form>
    );
  }
}


3. (deprecated) LinkedStateMixin というアドオンを使う

参考:Two-Way Binding Helpers | React (日本語)

冒頭で

ReactLink is deprecated as of React v15. The recommendation is to explicitly set the value and change handler, instead of using ReactLink.

と書いているので使わないと思うけど。

加えて、ES2015 で書いた React コンポーネントはそのままでは Mixin を使うことができないので
react-mixin というライブラリを使う。

参考:React v0.13から使えるようになったES6のclass構文でmixinを使う - Qiita

import React from 'react';
import reactMixin from 'react-mixin';
import { render } from 'react-dom';
import LinkedStateMixin from 'react-addons-linked-state-mixin';

class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };
  }

  render() {
    return (
      <form>
        <input
          type="text"
          valueLink={this.linkState('name')}
        />
      </form>
    );
  }
}
reactMixin(MyFormCmp.prototype, LinkedStateMixin);


4. (未検証) react-jsonschema-form を使う

元々は JSON Schema という、JSON オブジェクトの値の有無や型のチェックをするための仕様があって
react-jsonschema-form は JSON Schema からフォームを生成できるライブラリっぽい。

以下、JSON Schema の例。properties がフォームの項目に相当。

{
  "title": "A registration form",
  "description": "A simple form example.",
  "type": "object",
  "required": [
    "firstName",
    "lastName"
  ],
  "properties": {
    "firstName": {
      "type": "string",
      "title": "First name"
    },
    "lastName": {
      "type": "string",
      "title": "Last name"
    },
    "age": {
      "type": "integer",
      "title": "Age"
    },
    "bio": {
      "type": "string",
      "title": "Bio"
    },
    "password": {
      "type": "string",
      "title": "Password",
      "minLength": 3
    }
  }
}

実際にはこれとは別に UISchema なるものの定義が必要らしいが、ここから生成されるフォームはこんな感じ。
react-jsonschema-form playground で確認できる)

f:id:dackdive:20161019212542p:plain

いかんせんまだ触ったことがないので、スタイルやバリデーションルール含めどれぐらいきめ細かく設定ができるのか不明。


おわりに

3 は現在推奨されてないので除外。4 が一番気になってる。

1 と 2 については正直差はないと思う。
(JSX 側で state の key を知ってないといけない、という点で)

また、1 ~ 3 の方法は「入門 React」にも書いてた。(p.81 ~ p.84 あたり)

npm installしたパッケージの更新確認とアップデート(npm-check-updates)

タイトルの通り。
npm install --save なり --save-dev なりして package.json に書き込まれたパッケージのバージョン、どうやって定期的にアップデートしていけばいいかわからなかったので。

新しいバージョンがリリースされているかどうかの確認と、実際にどのように新しいバージョンにアップデートすればいいのか調べてみた。

今回サンプルに使う package.json

package.json の例として、以前、React のチュートリアルをやったときのリポジトリを使う。

{
  "name": "react-es6-tutorial",
  "version": "1.0.0",
  "description": "React Tutorial written in ES6",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "lint": "eslint src/**/*.js",
    "webpack": "webpack -w",
    "start": "concurrent \"npm run webpack\" \"npm run lite\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.7.2",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "concurrently": "^2.0.0",
    "eslint": "^2.9.0",
    "eslint-config-airbnb": "^9.0.1",
    "eslint-plugin-import": "^1.8.0",
    "eslint-plugin-jsx-a11y": "^1.2.0",
    "eslint-plugin-react": "^5.1.1",
    "webpack": "^1.12.14"
  },
  "dependencies": {
    "jquery": "^2.2.2",
    "marked": "^0.3.5",
    "react": "^0.14.7",
    "react-dom": "^0.14.7"
  }
}

この package.json に記載された通りのバージョン(babel-core なら 6.7.2)がインストールされた状態、という前提で話を進める。


現在インストールされているバージョンを確認する方法

そもそも、現在インストールされているバージョンを確認するにはどうしたらいいのか。
これは、後述する npm outdated コマンドを使うこともできるが、別の方法として

$ npm list --depth=0

コマンドを使うという方法もある。

$ npm list --depth=0
react-es6-tutorial@1.0.0 /Users/yamazaki/workspace/react/react-tutorial
├── babel-core@6.7.2
├── babel-loader@6.2.4
├── babel-preset-es2015@6.6.0
├── babel-preset-react@6.5.0
├── concurrently@2.0.0
├── eslint@2.9.0
├── eslint-config-airbnb@9.0.1
├── eslint-plugin-import@1.8.0
├── eslint-plugin-jsx-a11y@1.2.0
├── eslint-plugin-react@5.1.1
├── jquery@2.2.2
├── marked@0.3.5
├── react@0.14.7
├── react-dom@0.14.7
└── webpack@1.12.14

後述する npm outdated はバージョンが古くなったパッケージしか表示されないのに対し、
こちらはインストール済みのすべてのパッケージの情報が出力できる。


最新バージョンがあるかどうか確認する方法

インストールしたパッケージに新しいバージョンが存在するかどうか、確認する。
これは、

$ npm outdated

を使う。
https://docs.npmjs.com/cli/outdated

このコマンドを実行すると、以下のような結果が出力される。

f:id:dackdive:20161009200339p:plain

$ npm outdated
Package                 Current  Wanted  Latest  Location
babel-core                6.7.2  6.17.0  6.17.0  babel-core
babel-loader              6.2.4   6.2.5   6.2.5  babel-loader
babel-preset-es2015       6.6.0  6.16.0  6.16.0  babel-preset-es2015
babel-preset-react        6.5.0  6.16.0  6.16.0  babel-preset-react
concurrently              2.0.0   2.2.0   3.1.0  concurrently
eslint                    2.9.0  2.13.1   3.7.1  eslint
eslint-config-airbnb      9.0.1   9.0.1  12.0.0  eslint-config-airbnb
eslint-plugin-import      1.8.0  1.16.0   2.0.0  eslint-plugin-import
eslint-plugin-jsx-a11y    1.2.0   1.5.5   2.2.2  eslint-plugin-jsx-a11y
eslint-plugin-react       5.1.1   5.2.2   6.3.0  eslint-plugin-react
jquery                    2.2.2   2.2.4   3.1.1  jquery
marked                    0.3.5   0.3.6   0.3.6  marked
react                    0.14.7  0.14.8  15.3.2  react
react-dom                0.14.7  0.14.8  15.3.2  react-dom
webpack                 1.12.14  1.13.2  1.13.2  webpack

各パッケージに対し、Current, Wanted, Latest という3つの列が表示されている。
それぞれの列の意味は以下の通り。

Current

現在インストールされているバージョン

Wanted

存在するバージョンのうち、package.json に記載された semver の条件を満たす最新のバージョン。
たとえば、jQuery については

Package                 Current  Wanted  Latest  Location
jquery                    2.2.2   2.2.4   3.1.1  jquery

となっているが、これは 2.x 系の最新は 2.2.4 がリリースされており、それよりメジャーバージョンが1つ上の 3.x 系で 3.1.1 がリリースされている。
ただし、package.json^2.2.2 という記述では

2.2.2 <= n < 3.0.0

という範囲での最新バージョンしか許容されないため、Wanted は 3.x 系ではなく 2.x 系の最新バージョンとなる。

^2.2.2 という記述については以前ブログに書いた。
package.jsonのパッケージバージョンに記載される ^ (キャレット) とは?どうしてつくのか? - dackdive's blog


Latest

npm outdated のドキュメント によると

latest is the version of the package tagged as latest in the registry.

とあるが、そのパッケージの最新バージョンと考えてよさそう。


npm outdated の問題点

さて、この状態で npm update を実行し、再度 npm outdated で結果を確認してみる。

$ npm update
npm WARN peerDependencies The peer dependency react@^0.14.8 included from react-dom will no
npm WARN peerDependencies longer be automatically installed to fulfill the peerDependency
npm WARN peerDependencies in npm 3+. Your application will need to depend on it explicitly.
marked@0.3.6 node_modules/marked
babel-loader@6.2.5 node_modules/babel-loader

...(略)


$ npm outdated
Package                 Current  Wanted  Latest  Location
concurrently              2.2.0   2.2.0   3.1.0  concurrently
eslint                   2.13.1  2.13.1   3.7.1  eslint
eslint-config-airbnb      9.0.1   9.0.1  12.0.0  eslint-config-airbnb
eslint-plugin-import     1.16.0  1.16.0   2.0.0  eslint-plugin-import
eslint-plugin-jsx-a11y    1.5.5   1.5.5   2.2.3  eslint-plugin-jsx-a11y
eslint-plugin-react       5.2.2   5.2.2   6.3.0  eslint-plugin-react
jquery                    2.2.4   2.2.4   3.1.1  jquery
react                    0.14.8  0.14.8  15.3.2  react
react-dom                0.14.8  0.14.8  15.3.2  react-dom

babel-core などのパッケージは最新の 6.17.0 にアップデートされたため、表示されなくなった。

ただし、ESLint などについてはメジャーバージョンが上がったものがリリースされているが、npm update は package.json に記載された semver のルールに従うため、^ つきの記載だとメジャーバージョンのアップデートまでは行ってくれない。

また、npm outdated は パッケージの更新確認はやってくれるが、package.json の更新まではやってくれない という問題もある。
そのため、

  • npm outdated で新しいバージョンがリリースされてないか確認する
  • 新しいバージョンがリリースされていた場合、該当のパッケージを package.json から削除する
  • npm install --save/--save-dev で再度インストールする

という手順になってしまい、ややめんどくさい。
どうするか。


npm-check-updates を使う

というわけで色々調べていると、更新確認とアップデートに便利な npm-check-updates というパッケージを見つけた。

npm-check-updates は

$ npm install -g npm-check-updates

というようにグローバルにインストールする。インストールすると

$ ncu

というコマンドが使えるようになる。

ncu を実行してみる。

$ ncu
⸨░░░░░░░░░░░░░░░░░░⸩ ⠦ :
 jquery                   ^2.2.2  →   ^3.1.1
 react                   ^0.14.7  →  ^15.3.2
 react-dom               ^0.14.7  →  ^15.3.2
 concurrently             ^2.0.0  →   ^3.1.0
 eslint                   ^2.9.0  →   ^3.7.1
 eslint-config-airbnb     ^9.0.1  →  ^12.0.0
 eslint-plugin-import     ^1.8.0  →   ^2.0.0
 eslint-plugin-jsx-a11y   ^1.2.0  →   ^2.2.3
 eslint-plugin-react      ^5.1.1  →   ^6.4.0

The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, use ncu -a/--upgradeAll.

 marked                 ^0.3.5  →   ^0.3.6
 babel-core             ^6.7.2  →  ^6.17.0
 babel-loader           ^6.2.4  →   ^6.2.5
 babel-preset-es2015    ^6.6.0  →  ^6.16.0
 babel-preset-react     ^6.5.0  →  ^6.16.0
 webpack              ^1.12.14  →  ^1.13.2

Run ncu with -u to upgrade package.json

内容としては npm outdated を実行したときと同じ。
上部(The following... より上に列挙されているもの)は、新しいメジャーバージョンがリリースされているもの。
下部はマイナーバージョン以下で新しいバージョンが存在するもの。The following... の文章に書いてあるとおり、こちらは npm update すれば最新のバージョンがインストールできる。

そして、ncu に -u オプションをつけると package.json の更新が行われる。

$ ncu -u

...

Upgraded /Users/yamazaki/workspace/react/react-tutorial/package.json

$ git diff package.json
diff --git a/package.json b/package.json
index 11e9a27..f3e6979 100644
--- a/package.json
+++ b/package.json
@@ -17,18 +17,18 @@
     "babel-loader": "^6.2.4",
     "babel-preset-es2015": "^6.6.0",
     "babel-preset-react": "^6.5.0",
-    "concurrently": "^2.0.0",
-    "eslint": "^2.9.0",
-    "eslint-config-airbnb": "^9.0.1",
-    "eslint-plugin-import": "^1.8.0",
-    "eslint-plugin-jsx-a11y": "^1.2.0",
-    "eslint-plugin-react": "^5.1.1",
+    "concurrently": "^3.1.0",
+    "eslint": "^3.7.1",
+    "eslint-config-airbnb": "^12.0.0",
+    "eslint-plugin-import": "^2.0.0",
+    "eslint-plugin-jsx-a11y": "^2.2.3",
+    "eslint-plugin-react": "^6.4.0",
     "webpack": "^1.12.14"
   },
   "dependencies": {
-    "jquery": "^2.2.2",
+    "jquery": "^3.1.1",
     "marked": "^0.3.5",
-    "react": "^0.14.7",
-    "react-dom": "^0.14.7"
+    "react": "^15.3.2",
+    "react-dom": "^15.3.2"
   }
 }

package.json の更新が正しく行われた。

なお、ncu コマンドは

$ ncu -u [パッケージ名]

というようにパッケージ名を指定することができる。さらに、パッケージ名の部分には正規表現を使用することができる。

# babel-xxx というパッケージのみ対象
$ ncu /babel-/
⸨░░░░░░░░░░░░░░░░░░⸩ ⠏ :
The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, use ncu -a/--upgradeAll.

 babel-core           ^6.7.2  →  ^6.17.0
 babel-loader         ^6.2.4  →   ^6.2.5
 babel-preset-es2015  ^6.6.0  →  ^6.16.0
 babel-preset-react   ^6.5.0  →  ^6.16.0


また、ncu -c だと babel-core などマイナーバージョン以下がアップデートされたパッケージについては package.json は更新されなかったが、

$ ncu -a

というように、-u のかわりに -a オプションをつけて実行すると、これらのパッケージについても package.json が更新される。


注意点

npm-check-updates は package.json の更新のみ行い、実際のアップデートは行われていないので注意。
ncu -u した後 npm update する必要がある。


リファレンス

Dreamforce'16 Developer Keynoteのメモ

今年は行けなかったので Dev Keynote だけざっと見ました。

以下、雑多なメモ。

キーワード

  1. Lightning
  2. Einstein
  3. Salesforce DX


Lightning

Winter'17 以降の新機能についてデモしながら一通り紹介、といった感じ。
(デモは 13:40 頃から)

f:id:dackdive:20161007134941p:plain

Lightning Data Service

Visualforce で言う Standard Controller みたいな感じ。
Apex 側のコントローラーいらずでオブジェクトの参照や更新ができる。

f:id:dackdive:20161007135421p:plain

デモで紹介していたのは <force:recordPreview> というタグだったけど、
開発者ガイド見る限りいくつか種類があるっぽい。

リリースノート:
https://releasenotes.docs.salesforce.com/en-us/winter17/release-notes/rn_lightning_data_service.htm

開発者ガイド:
https://developer.salesforce.com/docs/atlas.en-us.204.0.lightning.meta/lightning/data_service.htm

JS のコントローラーではこのように書ける。
<force:recordPreview> コンポーネントを Id で見つけてきて、saveRecord() で保存)

f:id:dackdive:20161007135512p:plain


Lightning Action

Classic で言うカスタムボタンを追加して、そこから Lightning Component を表示できる。

f:id:dackdive:20161007135054p:plain

わかりづらいけどカーソルのある「Smart Home」がそう。


Lightning Utility Bar

f:id:dackdive:20161007135858p:plain

画面下部からいつでも呼び出すことのできる Lightning Component。かな?

所感


Einstein

機械学習プラットフォームの Einstein。

f:id:dackdive:20161007140033p:plain

デモがよくわからなかったんだけど、Apache Kafka と組み合わせてデータを学習モデルに放り込んでごにょごにょ...という感じ。

Apache Kafka については mokamoto さんの記事を読もう。

http://qiita.com/mokamoto/items/f453275efe6f056c98fb

KafkaはApache Foundationで開発されているpub-sub形式で分散メッセージング処理を取り扱うためのオープンソースミドルウェアです。 Producerと呼ばれるメッセージの送り手が、Kafkaの中にあるTopicに対してメッセージを送ります。Brokerはそのメッセージのハンドリング及び永続化を行い、ConsumerはTopicを購読してそのメッセージを受け取ることができます。

f:id:dackdive:20161007140203p:plain

train と predict だけ用意したら簡単に機械学習ができるのかな。

デモアプリ、これかと思ったけどたぶん違う。
http://www.dreamhouseapp.io/kafka/


Salesforce DX

f:id:dackdive:20161007140735p:plain

ようやく CI/CD 系の機能について発表がありました。
"Open and standard developer experience" とはまさに。

デプロイだけでなく組織の作成やデータのインポートなども行える CLI が提供されるっぽい。
sfdx ?? Force CLI はどうなるんでしょう。

f:id:dackdive:20161007140742p:plain

なんかいっぱいコマンドある。


デモでは、

f:id:dackdive:20161007140751p:plain

組織作成や

f:id:dackdive:20161007140755p:plain

組織にメタデータをプッシュ

のほか、組織からデータをエクスポートするといったこともやってました。

ただ、この機能については既に Twitter 上でいろいろ言われてるようなので
あまり期待しすぎずに詳細を待ちたいと思います。


おわりに:ロードマップ

f:id:dackdive:20161007140800p:plain

package.jsonのパッケージバージョンに記載される ^ (キャレット) とは?どうしてつくのか?

$ npm install --save react

などのコマンドでパッケージをインストールすると、package.json にはインストールした(その時点での最新)バージョンが記載されますが
そのとき

"dependencies": {
  "react": "^15.3.2"
}

というように、バージョン番号の前に ^ がつきます。

これの意味と、どうしてつくのか調べてみたメモ。

前提

Node.js のパッケージのバージョンは Semantic Versioning (semver) というルールに従っている。

semver は X.Y.Z という3桁の数字で表されるバージョンで、

  • X: major version。後方互換性のない変更の場合にこのバージョンを上げる
  • Y: minor version。後方互換性のある変更の場合、このバージョンを上げる
  • Z: patch version。後方互換性のあるバグ修正などの場合、このバージョンを上げる

というルールになっている。


^ (キャレット) とは?

このあたりを読む。
https://docs.npmjs.com/misc/semver#caret-ranges-123-025-004

Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple

パッケージのバージョン番号のうち、一番左の非ゼロの値を変更しないようなアップデートを許容する、という意味のよう。

つまり、

  • バージョンが 1.2.3 の場合、1.2.3 <= n < 2.0.0
  • バージョンが 0.2.3 の場合、0.1.2 <= n < 0.2.0
  • バージョンが 0.0.3 の場合、0.0.3 <= n < 0.0.4

というように、対象のバージョンによって挙動が異なる。

そして、npm install や npm update を行ったとき、許容される範囲の中で最新のバージョンをインストールしたりアップデートしたりする。
そのため、package.json だけだとバージョンを完全に固定できているわけではない


【補足】package.json でバージョンを完全に固定するには?

後述するように ^ をつけないように設定を変更するか、npm shrinkwrap コマンドを使って npm-shrinkwrap.json というファイルを生成しておく。
https://docs.npmjs.com/cli/shrinkwrap

// npm-shrinkwrap.json (例)
{
  "name": "react-es6-tutorial",
  "version": "1.0.0",
  "dependencies": {
    "jquery": {
      "version": "2.2.2",
      "from": "jquery@2.2.2",
      "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.2.tgz"
    },
    "marked": {
      "version": "0.3.5",
      "from": "marked@0.3.5",
      "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz"
    },
    "react": {
      "version": "0.14.7",
      "from": "react@0.14.7",
      "resolved": "https://registry.npmjs.org/react/-/react-0.14.7.tgz",
      "dependencies": {
        "envify": {
          "version": "3.4.1",
          "from": "envify@>=3.0.0 <4.0.0",
          "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.1.tgz",
          "dependencies": {
            "through": {
       ...


【補足】~ (チルダ)

^ と似たような記号として、~ (チルダ) もある。
https://docs.npmjs.com/misc/semver#tilde-ranges-123-12-1

Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.

minor version が明記されていれば patch version のアップデートを許容、明記されていなければ minor version のアップデートを許容。
なので、

  • ~1.2.3 の場合: 1.2.3 <= n < 1.3.0
  • ~1.2 の場合: 1.2 <= n < 1.3.0
  • ~1 の場合: 1.0.0 <= n < 2.0.0


^ (キャレット) はなぜつくのか?

package.json にデフォルトで ^ がつく挙動はどこで設定しているのだろうか?気になって調べてみた。
どうやら npm config に関係する設定があるらしい。


save-prefix

デフォルトでつく prefix を設定する。

$ npm config get save-prefix
^

# デフォルトを ~ に変更
$ npm config set save-prefix "~"
$ npm config get save-prefix
~


save-exact

--save--save-dev したときに prefix をつけるか、あるいは prefix をつけずに package.json に記載してバージョンを完全に固定するかを選べる。
デフォルトは false。なので prefix がつく。

$ npm config get save-exact
false

# デフォルトで完全なバージョン固定になるようにする
$ npm config set save-exact true


【補足】npm config で設定した値

どうやらこの設定は ~/.npmrc に保存されているらしい。
.npmrc はプロジェクトごとに置くことも可能。
https://docs.npmjs.com/misc/config#npmrc-files


^ (キャレット) をつけずにインストールする方法はないのか?

上述したように、npm config で

$ npm config set save-exact true

と設定しておくと、npm install 時の挙動を変更することができる。

あるいは、npm install 時のオプションとしても

--save-exact (略記は -E)

が用意されている。
https://docs.npmjs.com/cli/install

これをつけると、^ なしでインストールできる。

# -SE は --save --save-exact の略
npm i -SE react

Hubotに管理画面のような静的ページを追加する

メモ。
ブラウザから /admin にアクセスしたら管理画面みたいなものが開いて Bot の簡単なカスタマイズができる、みたいなことがやりたくて
特定の URL で静的なページを返すようなことができるのか調べてみました。

結論から言うと robot.router が Express サーバーのインスタンスになっているようなので、この変数に対して通常の Express アプリ開発と同じようなノリでできそうです。

公式ドキュメントのこちらに記載があります。
https://hubot.github.com/docs/scripting/#http-listener

Hubot includes support for the express web framework to serve up HTTP requests. It listens on the port specified by the EXPRESS_PORT or PORT environment variables (preferred in that order) and defaults to 8080.

また、Hubot のソースコードで言うとこのあたり。
https://github.com/github/hubot/blob/master/src/robot.coffee#L411

サンプルコード

CoffeeScript ではなく ES2015 で書いています。
参考:HubotをES2015で書いてHerokuにデプロイする - dackdive's blog

module.exports = (robot) => {
  robot.router.get('/admin', (req, res) => {
    res.end('Hello, World!');
  });
};
結果

f:id:dackdive:20160806113925p:plain

表示できました。

注)
$ ./bin/hubot

を直接実行するとデフォルトでポートは 8080 番が使われますが、この時は

$ heroku local web

# Procfile は以下
web: bin/hubot -a slack --require build

コマンドを使い、Heroku アプリのローカル実行として実行しています。
heroku local コマンドでアプリを起動した時、デフォルトのポートは 5000 番なので キャプチャでは :5000 にアクセスしています。


ejs などのテンプレートエンジンも使ってみる

スクリプト内に HTML をゴリゴリ書いていくのはつらいので、JavaScript のテンプレートエンジンも使えるか試してみます。
Express とテンプレートエンジンの使い方についてはこちらの記事を参考にします。

ゼロからはじめるExpress + Node.jsを使ったアプリ開発 - Qiita

$ npm i --save ejs

でインストールします。

スクリプトとテンプレートファイルのディレクトリ構成は以下のようになっています。

.
├── Procfile
├── README.md
├── bin
│   ├── hubot
│   └── hubot.cmd
├── build ...................... src をトランスパイルしたスクリプト
│   └── index.js
├── external-scripts.json
├── package.json
├── src
│   └── index.js
└── views ...................... テンプレート置き場
    └── index.ejs
スクリプト&テンプレート
// src/index.js
module.exports = (robot) => {
  robot.router.set('view engine', 'ejs');

  robot.router.get('/admin', (req, res) => {
    res.render('index', {});
  });
};
<!-- views/index.ejs -->
<html>
<body>
    <h1>This is Admin Page!</h1>
</body>
</html>
結果

f:id:dackdive:20160806115757p:plain

できました。


今後の予定

まだ軽く試しただけなので、このまま通常の Express アプリ開発と同じように、JS ライブラリや UI フレームワークを使って静的サイトを構築していけるのかはわかりません。
あと、このページで Bot のメッセージ定型文を登録・管理できるように...とかしたくなったら、データの永続化方法とかも調べてみないといけませんね。

Persistence の項を読むと hubot.brain という KVS を提供しているみたいだけど、Heroku で使おうとするとどうなるのか。

Slackのチャンネルのメンバーからランダムで一人選ぶBotを作る

タイトルの通り。

  • GitHub の PR のレビュアーを、開発チームの特定のメンバー数名から一人指名したい
  • 開発チームの一部のメンバーで持ち回りでやらないといけないタスクがあって、毎回誰がやるか決めるのめんどい

といったことがあって、せっかくなので bot に決めてもらえないかなあという気分になりました。

「どの人たちから選ぶか」は目的によってまちまちなので、それなら Slack のチャンネルのメンバーから選ぶようにすれば汎用性が高いんじゃないかと思って作ってみました。

コード

先にコードから。

なお、フレームワークとして Hubot を使ってますが、CoffeeScript を書くのが嫌だったので ES2015 で書いてます。
そのあたりの設定の話はこちらを参考にしてください。

dackdive.hateblo.jp

import request from 'request';

module.exports = (robot) => {
  robot.respond(/.*(random|抽選|選ぶ).*/, (msg) => {
    const url = 'https://slack.com/api/channels.list?token=' + process.env.HUBOT_SLACK_TOKEN;

    // チャンネル一覧を取得
    request(url, (err, res, body) => {
      // msg.message.room で現在の channel 名が取れる
      const channel = findChannel(JSON.parse(body).channels, msg.message.room);
      console.log('Channel found');
      console.log(channel);

      // bot 自身を除外して抽選
      const botId = robot.adapter.self.id;
      const filterdMembers = channel.members.filter((member) => {
        return member !== botId;
      });
      console.log(filterdMembers);
      const member = msg.random(filterdMembers);

      msg.send('選ばれたのは、 <@' + member + '> でした :tea:');
    });
  });
}

function findChannel(channels, targetName) {
  for (const channel of channels) {
    if (channel.name === targetName) {
      return channel;
    }
  }
  return null;
};

GitHub:

(PR は #1


動作の様子

f:id:dackdive:20160728184414p:plain

bot に向かって random/抽選/選ぶ といった単語を含めてメンションすると適当に選んでくれます。


実装上のポイントなど

現在のチャンネル名は msg.message.room で取れる

最初に苦労したのは、自分が今いるチャンネルを取得するという処理です。
以下の記事を読んで msg.message.room で取れるんだーということを知りました。

The Hubot msg Object

自分でも console.log(msg) して中身を確認してみました。
message 以外にも robot に色々情報が入ってそうです。

Response {
  robot:
   Robot {
     name: 'hubot',
     events:
      EventEmitter {
        ...
        _maxListeners: undefined },
     brain:
      Brain {
        ...
        _eventsCount: 3 },
     alias: false,
     adapter:
      SlackBot {
        customMessage: [Function],
        message: [Function],
        ...
  message:
   SlackTextMessage {
     user:
      User {
        id: 'U1R73F7DL',
        name: 'zaki-yama',
        ...
        room: 'bot-channel' },
     text: '@hubot: random',
     rawText: '<@U1R7NPEKU>: random',
     rawMessage:
      Message {
        _client: [Object],
        _onDeleteMessage: [Function],
        deleteMessage: [Function],
        _onUpdateMessage: [Function],
        updateMessage: [Function],
        type: 'message',
        channel: 'C1VTH4FPD',
        user: 'U1R73F7DL',
        text: '<@U1R7NPEKU>: random',
        ts: '1469699396.000062',
        team: 'T1R7BJHV3' },
     id: '1469699396.000062',
     done: false,
     room: 'bot-channel' },


Slack の channel オブジェクトからチャンネルに所属するメンバーを取得する

Slack APIchannels.info でチャンネル一覧を取得し、チャンネル名が msg.message.room と一致するものを探します。

レスポンスの channels プロパティの中にすべてのチャンネルが配列で入ってます。
また、channel オブジェクトの中身はこちらに記載されています。
https://api.slack.com/types/channel

channel.members でメンバーの一覧が取得できますね。


Bot 自身の情報は robot.adapter.self.id で取れる

Bot もチャンネルに含まれているため、ランダムで一人選ぶときには自分を除外しなければなりません。
このためだけに Slack API を叩かないとだめなのかなーと思っていたんですが、なんとなく robot.adapter あたりを console.log() で見てたら見つかりました。

このあたりのドキュメントはどこかにまとまってるんですかね。。。


配列からランダムで1つ選ぶ、は msg.random(array) が使える

らしいです。Hubot の公式ドキュメントに書いてあります。
https://hubot.github.com/docs/scripting/#random

(Hubot のドキュメントは見づらい...!)


メンションは名前でもIDでも送れる

channels.info で取得した channel オブジェクトの members からはユーザーの ID しか取れないので、
てっきり最初は別途 users.info を叩いてユーザー名を取得し直さないといけないんだと思ってました。

が、Slack API のドキュメントをちゃんと読むと、メンションをとばすときは ID で OK ということが書かれています。

https://api.slack.com/docs/message-formatting#linking_to_channels_and_users

むしろ

The readable name can be included after the ID, by separating them with a pipe (|) character.

なので、ID の後ろに name をつけても良いよ、ぐらいでしょうか。
ただ、 ユーザー名だけでも一応メンションはとばせるみたいです。

メンションは @userId でなく <@userId> とする必要があるみたいですね。


TODO

毎回同じ返事だと寂しいので、メッセージにバリエーションを持たせたいです。
→対応しました。GitHub 参照 https://github.com/zaki-yama/hubot-es2015/issues/3


リファレンス