Babel 7 を試す

2018年9月11日 0 開発 , ,

2018/8/27 に Babel 7 がリリースされたので既存プロジェクトに導入してみたので変更や問題点などをまとめる。

参考資料とサンプル プロジェクトは以下。

Scoped Packages

Babel 公式の npm が Scoped Packages に変更された。scope は @babelbabel-XXXX@babel/XXXX という命名へ移行された。私のよく利用するものだと以下のようになる。

{
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "@babel/register": "^7.0.0"
  }
}

第三者の Babel preset/plugin について babel-XXXX 命名を維持させながら、公式 npm を明確に区別できるようになった。webpack など npm で第三者の拡張を許可しているものに、この方法が広まってほしい。

babel.config.js

これまで Babel に関する設定はプロジェクトの package.jsonbabel プロパティーで記述していた。しかしこんな議論があったのと、Babel の設定ファイルとして JavaScript サポートが追加されたので移行することにした。

ファイル名は babel.config.js。機能は Config Files で解説されている。移行後の内容は以下。

module.exports = (api) => {
  const presetEnv = [
    '@babel/preset-env',
    {
      targets: {
        electron: '2.0'
      }
    }
  ]

  return {
    presets: api.env('development') ? [
      presetEnv,
      '@babel/preset-react',
      'power-assert'
    ] : [
      presetEnv,
      '@babel/preset-react'
    ]
  }
}

新形式は JavaScript というか Node モジュールとして定義する。このファイル自体は Babel の対象にならないようなので export default ではなく module.exports としている。アロー関数は現時点の Node LTS である v8.x 系なら対応しているので採用。

モジュールとする関数には api という引数が渡される。これは命名そのままで Babel を操作したり環境情報を取得するためのもの。

api.env() を呼ぶと現在の環境変数名が得られる。api.env('環境変数名') とした場合は、それが指定されていれば true を返す。これを利用して development 時だけ power-assert を処理するようにした。

webpack の DefinePlugin が機能しない?

サンプル プロジェクトでは Babel と webpack を組み合わせている。そして Babel 7 対応は npm 更新と Babel 関連の設定変更だけでよい認識していた。実際、正常に動作する。

しかし babel.config.js 移行にともない webpack の DefinePlugin による development 分岐が本当に機能しているか不安だったので、ログをとってみた。

module.exports = (api) => {
  console.log(api.env())
  // ...後略
}

webpack の設定は以下。

new WebPack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('production')
})

この状態で developmentproduction 両方のビルドを試してみたところ、標準出力には いずれも development が表示されるではないか。webpack の GitHub issues を調べてみたら #2121 を見つけた。issue コメントにあった設定例をみると JSON.stringify せず直に 'production' を設定していたので、そのように修正してみたが結果は変わらず。

気づいていなかっただけで従来の package.json における babel.env 分岐も失敗していた可能性がある。仮に power-assert が処理されても assert が置き換えられるぐらいだろうし、テスト コードは実装側から参照していないため害はなさそう。しかし意図しない動作になっているのは怖いので対策してみた。

方法は単純。webpack 処理で process.env.NODE_ENV を設定するだけ。webpack 処理はすでに --modedevelopmentproduction を分岐していたので

export default (env, argv) => {
  const MAIN = !!(env && env.main)
  const PROD = !!(argv.mode && argv.mode === 'production')
  if (PROD) {
    process.env.NODE_ENV = 'production'
  }
}

とした。これをそれぞれの --mode で実行したところ、babel.config.js 側の api.env() が想定どおりの値を返すことが確認できた。

ちなみに webpack の設定ファイルを webpack.config.babel.js にすると Babel 変換されるため babel.config.js が呼ばれる。しかし webpack に --mode production を指定してもこちらのログは当然ながら常に development だった。

環境変数を npm-scripts から分岐するために cross-env を利用しているなら、webpack CLI より前に production を設定できるかもしれない。ただし webpack を利用しているならビルド系の設定を webpack に集約するほうが管理しやすいのでそうしている。

mocha + power-assert

Babel の Scoped Packages 化にともない npm-scripts へ定義していた mocha + power-assert 実行コマンドも対応させる。まずは関連 npm を更新。

{
  "devDependencies": {
    "babel-preset-power-assert": "^3.0.0",
    "mocha": "^5.2.0",
    "power-assert": "^1.6.0"
  }
}

続けてコマンド。

{
  "scripts": {
    "test": "mocha --require @babel/register src/js/**/*.test.js",
  }
}

--require へ指定するモジュールを babel-register から @babel/register に変えるだけで正常に動作した。

まとめ

いくつか問題はあったものの Babel 公式資料が充実していることもあり、さほど苦労せず移行できた。他のプロジェクトも順次 Babel 7 にする予定。

破壊的な変更があるため、このブログも含む古い記事を読むと混乱するかもしれない。ソフトウェアに関する調べ物をするときに資料の日時と言及対象のバージョンを確認するのは鉄則。とはいえ利用する機会のありそうなものならメジャー更新は定点観測するようにしておくと、知識更新の差分を小さくできてよい感じ。

今回は試さなかったけれど Babel 7 の目玉機能に TypeScript 対応がある。私としては ECMAScript で型定義が標準化されることを望んでいる。しかし vscode で優遇されていることもあり TypeScript に移行したくなってきた。あとで試すかも。

npm rewire を試す

2018年7月18日 0 開発 ,

最近 React SFC (Stateless Functional Components) や Redux の影響を受けて、ES.next な環境でもクラスより関数で実装するようにしている。ES Modules であれ Node であれ個別に関数を外部公開できるからクラスを使用せずとも実装には十分だし、むしろ関数を積極的に採用することで外部依存を引数に限定できる。

しかし困ったことが。非公開の関数を単体テストする手段がない。

そもそも非公開なものをテスト対象とするのは悪手では?と言われればそのとおり。しかし公開関数が単一単純でも内部で多くの非公開な関数へ依存しているなら、それらを個別に単体テストしたくなるだろう。外部からみたら公開関数を単体テストしているつもりでも実際には結合テストなわけで。

クラスなら今のところアクセス指定子がないため、非公開メソッドは命名を工夫する慣習 (アンダースコアを接頭辞にする) があるだけだから

export default class Sample {
  publicMethod () {
  }

  _privateMethod () {
  }
}

と定義してあれば _privateMethod を呼び出せる。

一方、モジュール スコープに定義された非公開の関数を外部参照するためには Java や C# でいうところのリフレクション的な処理が必要になる。というわけで、ようやく rewire の話。

rewire

非公開な関数をテストする方法やツールについて調べたたら以下の記事をみつけた。

まさにほしかったもの。記事が書かれたのは 2013 年ということもあり ES2015 以降で動くのか不安だったが

を見ると現在もメンテされているようだ。では試してみよう。まずは環境から。package.json からテストに関するものを抜粋。

{
  "scripts": {
    "test": "mocha --timeout 50000 --require babel-register src/**/*.test.js"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-power-assert": "^2.0.0",
    "babel-register": "^6.26.0",
    "mocha": "^5.2.0",
    "power-assert": "^1.6.0",
    "rewire": "^4.0.1"
  }
}

この環境で shortcode.js というファイルに定義された以下の非公開 API をテストしてみる。

const trimLineBreak = (text) => {
  return text.replace(/^[\n]|[\n]$/g, '')
}

テスト コードは以下。

import assert from 'assert'
import Rewire from 'rewire'

describe('trimLineBreak', () => {
  const Module = Rewire('./shortcode.js')
  const trimLineBreak = Module.__get__('trimLineBreak')

  it('trimLineBreak', () => {
    let actual = trimLineBreak('\nText\n')
    assert(actual === 'Text')

    actual = trimLineBreak('\n\nText\n')
    assert(actual === '\nText')
  })
})

npm test を実行すると非公開にも関わらず trimLineBreak を呼び出しテストは成功。assert の引数を書き換えて意図的にテストを失敗させても、きちんと power-assert による失敗の詳細が表示された。

rewire の使い方について。まずは rewire を読み込む。

import Rewire from 'rewire'

次に非公開の関数が定義されているモジュールを rewire 経由で読み込む。

const Module = Rewire('./shortcode.js')

最後にモジュールから非公開 API の名前を指定して参照を得る。

const trimLineBreak = Module.__get__('trimLineBreak')

これで非公開 API を呼び出せる。なお関数だけでなく変数も読み込めるため、テストに非公開な定数が必要な場合でも rewire でいける。実に便利。

余談だがクラスの非公開メソッドについて。tc39/proposals によると 2018/7 時点で

が Stage 3 なので近いうちに事情が変わる可能性はある。これに対して rewire が効くのかは babel-preset-env にきたら改めて試すかも。

npm icon-gen v1.2.1 release

2018年4月2日 0 開発 ,

icon-gen v1.2.1 をリリースした。

icon-gen 本体については特に機能追加とかバグ修正はないのだけど PNG ファイルから画素を抽出するのに利用している pngjs が Node v9.x 系で動作しないため、代替として pngjs-nozlib へ乗り換えた。

これは pngjs の fork 版で Node 標準の zlib 依存を排除したもの。npmjs からの repository リンクだと本家 pngjs 側になっているけどコードは mikolalysenko/pngjs に公開されている。

pngjs の問題は元々、icon-gen の issue として 2017/11/27 に報告されていた。

これを報告者の lmm-git (Leonard Marschke) 氏が pngjs 本家 issue としても登録。

そして現時点では Pull Request をもって close。pngjs v3.3.2 へ反映されたのだが、残念なことに直っていない。icon-gen の dependencies を更新しても再発するので v3.3.2 時点の pngjs を clone してユニット テストを走らせたも、やはり通らない。

というわけで関連 issue をたどって見つけた pngjs-nozlib を採用することにしたのだった。ちなみに pngjs 作者の lukeapage (Luke Page) 氏いわく、

I closed it because a contributor claimed their Pr fixed it.. sorry it didn’t.

I don’t actively maintain pngjs any more.. I’m happy to merge prs and release but I don’t have time to fix this. If you send a Pr I will merge it.

とのこと。Pull Request がきたら採用するけれど自身として積極的に修正することはしないそうだ。本家 issue の関連や npmjs 上の Dependents を見るとわかるように pngjs は相当に著名なツールなのだけど、そうしたものでもやはり個人開発だと運用持続に期待するのは厳しいなぁ、と感じた。

かくいう私も例外ではない。せっかく issue 登録してもらっても長く放置することがある。趣味プロジェクト運用は私生活や気分に左右されやすい。最近だとエレキ ギターを十数年ぶりに再開したこともあって趣味プロジェクトに注いでいた気力をだいぶ奪われてる。

著名プロジェクトなら Organization にすれば?という意見もあるだろう。それは分かるけど Organization もなんだかんだコミュニティー運営することになるわけで、それも結構な気力を使う。どうしても機能追加とかバグ修正したいユーザーがあれば Pull Request してくれるだろうし、それ待ちでいいやとなるのはよくわかる。

以前 Takuto Wada(@t_wada)さんが書いた

を思い出した。pngjs は「良いソフトウェア」である。一方で今回の問題は Node v9.x で破壊的な変更が入って巻き添えを受けた形になる。こうした参照される側の影響で「良さ」以前に互換性が壊されたとき、それに対する責任は参照する側に問われるべきだろうか。LICENSE テキストに書かれた免責事項はどれぐらい有効なのか。

気分がのったとき自分のほしいものを自己責任で公開して適当に雑にゆるやかに運用してゆくだけ。そんな OSS はたくさんありながらもそうと認識されておらず、ある日とつぜん問題が顕在化して騒ぎになるのだろう。これからも。

免責事項により参入障壁は下がりプレーヤー増加と競争を促している側面がある。強く責任を求められる文化だったらここまで OSS が隆盛することはなかっただろう。いつか誰かが作り直せばいい。以前よりも少しだけ前進すればいい。という楽観的で寛容な文化と責任のせめぎあい。

などと、とりとめもなく考えてみたものの結論はない。有効な手立ても思いつけないでいる。