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 に移行したくなってきた。あとで試すかも。

espower-babel から babel-preset-power-assert への移行

2016年4月18日 0 開発 , , ,

私は Web フロントエンドのテストで mocha + power-assert + espower-babel を組み合わせて利用しているのだが、これらのうち espower-babel が Deprecated になった。経緯と代替については以下の記事にまとまっている。

これからは espower-babel 代わりに babel-registerbabel-preset-power-assert を利用する。babel-register で require をフックして assert を power-assert に置き換える、この処理が Babel ビルドの一環として実行されるようになった、という理解でよいのだろうか。

移行には npm 更新、.babelrc と mocha.opt 修正が必要。これを一発で実行するために migrate-espower-babel-to-babel-preset-power-assert というツールも用意されている。冒頭の記事にも使用方法が掲載されているけど、自分でも試したいので、その内容を以下にまとめる。

migrate-espower-babel-to-babel-preset-power-assert

このツールは既に mocha + power-assert + espower-babel を利用してるプロジェクトを新構成へ移行するためのもの。複数プロジェクトで利用したいのでグローバルにインストールする。

$ npm i -g migrate-espower-babel-to-babel-preset-power-assert

次に移行したいプロジェクトの package.json の置かれた階層へ移動する。そして以下のようにコマンドを実行。

$ migrate-espower-babel-to-babel-preset-power-assert
Run: npm uninstall --save-dev espower-babel
Run: npm install --save-dev power-assert
Run: npm install --save-dev babel-preset-power-assert
Run: npm install --save-dev babel-register
rewrite mocha.opts
rewrite .babelrc

注意点がある。

test ディレクトリ内に mocha.opts が存在しないとエラーになる。また、空の mocha.opts を配置した場合、出力には rewrite と表示されるがファイルは空のまま。---compilers js:espower-babel/guess を定義してから実行したら ---compilers js:babel-register に書き換えられた。

私は mocha.opts を利用せず npm-scripts の test で対応していたので、エラーに遭遇してしまった。mocha について、これを期に mocha.opts を使うようにするか npm-scripts 完結のままにするか迷っている。

あと、このコマンドを繰り返し実行すると .babelrc の env.development.presets に power-assert が複数追加される。移行ツールなのでコマンド実行は一度で十分だが、mocha.opts の有無による動作検証で何度も実行していたらこの問題に遭遇した。

手動による導入 or 移行

移行ツールでおこなっていることをそのまま手動で実行する。

はじめに espower-babel をアンインストールする。このコマンドは npm がなければ空振りするだけなので、導入時に実行しても問題ない。

$ npm uninstall -D espower-babel

テストに必要な npm をインストールする。テスト対象とテストの両方を ES2015 で書き、かつ React の JSX を含むので、そのたけのプリセットも含めている。

$ nmp i -D mocha power-assert babel-preset-power-assert babel-register
$ npm i -D babel-preset-es2015 babel-preset-react

package.json と同一階層に .babelrc を作成、以下を記述して保存。

{
  "presets": [
    "es2015",
    "react"
  ],
  "env": {
    "development": {
      "presets": [
        "power-assert"
      ]
    }
  }
}

mocha のオプションを test ディレクトリ内の mocha.opts で指定するならファイルを作成、以下を記述して保存。

---compilers js:babel-register
test/**/*.test.js

私のように mocha の実行とオプションを package.json の npm-scripts で一括指定するなら、以下のように記述。mocha.opts ありの場合、test コマンドの内容はオプションなしで mocha だけにすること。

{
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js"
  }
}

mocha.opts、npm-scripts ともに babel-register のフックを有効にして test 配下の .test.js というファイルをテスト対象にしている。わざわざ特別な拡張子にしている理由は以下。

  • テキスト エディタのタブ上でテスト対象とテストを区別したい
  • モックやユーティリティなど、テストから参照されるがテスト コードを含まないファイルをスキップしたい

既存プロジェクトで移行が正常であることを試すため test コマンドを実行。

$ npm test

> electron-starter@1.0.7 test .../examples-electron/electron-starter
> mocha



  Util
    formatDate
      ✓ Default YYYY-MM-DD hh:mm:ss.SSS
      ✓ Hyphen YYYY-MM-DD-hh-mm-ss
      ✓ No zero-padding YYYY/M/D h:m:s


  3 passing (17ms)

ばっちり。npm-scripts のみと mocha.opts の両方を実行してみたが、どちらも正常にテスト実行された。

ES2015 と assert

今回のテストに使用したコードは以下となる。

import assert from 'power-assert';
import Util   from '../../src/js/common/Util.js';

/** @test {Util} */
describe( 'Util', () => {
  /** @test {Util#formatDate} */
  describe( 'formatDate', () => {
    it( 'Default YYYY-MM-DD hh:mm:ss.SSS', () => {
      const date = new Date( 2015, 7, 4, 21, 17, 45, 512 );
      const text = Util.formatDate( date );
      assert( text === '2015-08-04 21:17:45.512' );
    } );

    it( 'Hyphen YYYY-MM-DD-hh-mm-ss', () => {
      const date = new Date( 2015, 7, 4, 21, 17, 45, 512 );
      const text = Util.formatDate( date, 'YYYY-MM-DD-hh-mm-ss' );
      assert( text === '2015-08-04-21-17-45' );
    } );

    it( 'No zero-padding YYYY/M/D h:m:s', () => {
      const date = new Date( 2015, 7, 4, 21, 17, 45, 512 );
      const text = Util.formatDate( date, 'YYYY/M/D h:m:s' );
      assert( text === '2015/8/4 21:17:45' );
    } );
  } );
} );

見てのとおりテスト自体も ES2015 で記述されている。espower-babel と同じく、テストと対象の両方で ES2015 を利用できる。プリセットを変えれば ES2016 や Polyfill などもゆける。

また、

import assert from 'power-assert';

import assert from 'assert';

でもよい。実際に import を標準 assert へ書き換えてから、わざとテストを失敗させてみる。

  1 failing

  1) Util formatDate Default YYYY-MM-DD hh:mm:ss.SSS:

      AssertionError:   # test/common/Util.test.js:11

  assert(text === '2015-08-04 21:17:45.512')
         |    |
         |    false
         "2016-08-04 21:17:45.512"

  --- [string] '2015-08-04 21:17:45.512'
  +++ [string] text
  @@ -1,12 +1,12 @@
   201
  -5
  +6
   -08-04 2


      + expected - actual

      -false
      +true

      at decoratedAssert (node_modules/empower/lib/decorate.js:42:30)
      at powerAssert (node_modules/empower/index.js:65:32)
      at Context.<anonymous> (Util.test.js:11:7)

エラー情報が詳細に表示されている。冒頭の記事で解説されていたように、assert そのものが power-assert 化されていることがわかる。

assert を import するようにしておくと、将来 power-assert から標準 assert へ戻したくなった時、コードを書き換えずに済む。例えば標準 assert が power-assert 並に高機能化するとか、むしろ power-assert の実装を取り込むとかしたとき、脱却することになるかもしれない。

まとめ

espower-babel に比べて設定量は増えたが、Babel を使うなら .babelrc は必要なので、そこに集約したほうが分かりやすくなる。

プロジェクトにあまりファイルを増やしたくないので、mocha.opts については採用すべきか迷うところだが、既に .babelrc だけでなく esdoc.json なんかも追加しているので、そういうものだと受け入れることにした。

Babel については package.json の babel プロパティでも代替できるそうだが、その他のツールもこの方式をサポートしていなと管理がバラバラになりかえって混乱しそうだ。私の理想は

{
  "tools": {
    "babel": {},
    "browserify": {}
    "esdoc": {},
    "mocha": {}
  }
}

のような感じでツール専用のプロパティが用意され、その配下に Babel などのプロパティが属する設計。しかし既に実運用されているものを後付で統一されたルールに移行させるのは高い政治力が求められるし、この方式による package.json の巨大化は受け入れられなさそうだから、妄想にとどめておく。

babel-preset-power-assert 移行により、テスト対象の assert も power-assert 化されるのはよい。デバッグ ビルドだけ assert を有効にする ( = リリース ビルドでは除去 ) なら邪魔にならないし、テスト時に assert がヒットしたら詳細な情報にあたれるので、assert を積極的に書きたくなる。

最近の Web フロントエンド開発はビルドとプリプロセッサーを前提とすることで IDE のように高度な依存注入や変換を利用できて生産性が増している。ありがたいことだ。

最後に移行の実装例となるプロジェクトを挙げておく。

power-assert を import する時の注意点

2015年10月15日 2 開発 , , ,

小ネタ。

前に ES6 コードをテストする という記事を書き、以降は power-assertespower-babel を利用していた。これらを使うとテスト対象とテストの両方を ES6 で記述できて便利である。

しかし最近、テストが Fail な時に power-assert の特徴である詳細なエラー表示にならず標準 assert のような出力になってしまい、地味に困っていた。

そこで原因を調べたところ、前に記事を書いたときは power-assert の import を

import assert from 'power-assert';

のようにしていたが標準 assert になってしまう。テスト コードでは

import Assert from 'power-assert';

のように assert を PascalCase で Assert としていることがわかった。これを assert に修正して実行すると、従来どおり詳細なエラー表示が復活した。比較を分かりやすくするため、以下の Fail になるテストを実行してみる。

import assert from 'power-assert';
import Assert from 'power-assert';

describe( 'power-assert import', () => {
  it( 'assert', () => {
    assert( 'a' === 'b' );
  } );

  it( 'Assert', () => {
    Assert( 'a' === 'b' );
  } );
} );

出力は以下のようになった。

$ npm test

> electron-audio-player@1.0.0 test .../sample
> mocha --compilers js:espower-babel/guess test/**/*.test.js

  power-assert import
    1) assert
    2) Assert

  0 passing (44ms)
  2 failing

  1) power-assert import assert:

      AssertionError:   # test/Sample.test.js:6

  assert('a' === 'b')
             |
             false

  --- [string] 'b'
  +++ [string] 'a'
  @@ -1 +1 @@
  -b
  +a

      + expected - actual

      -false
      +true

      at decoratedAssert (node_modules/empower/lib/decorate.js:42:30)
      at powerAssert (node_modules/empower/index.js:58:32)
      at Context.<anonymous> (test/Sample.test.js:6:5)

  2) power-assert import Assert:

      AssertionError: false == true
      + expected - actual

      -false
      +true

      at decoratedAssert (node_modules/empower/lib/decorate.js:44:25)
      at powerAssert (node_modules/empower/index.js:58:32)
      at Context.<anonymous> (test/Sample.test.js:10:5)

npm ERR! Test failed.  See above for more details.

assert のほうは詳細な情報が出力されている。しかし Assert では標準 assert と同じく、情報量が少ないことが分かる。

ところで冒頭に「最近」と書いた。けれど前からこの書き方をしていたので、もっと早く気づいてもよさそうなものだ。しかしテストで Fail に遭遇する機会が少なく、また Fail になった時もすぐに原因が判明して修正したため違和感はあっても放置してしまった。

それと ES6 の import でブラケットを使用しない場合、変数は export 対象へのエイリアスとして機能すると認識していたから名前の違いが動作に影響するとは考えなかった。この問題が起きても標準 assert としては動作するため、power-assert の一時的な出力のバグ ( いつか直るもの ) と勘違いもしていた。

ちなみに import 先の変数を PascalCase にしている理由は、単なる好みである。import はファイルの先頭に書かれて広範囲に参照されるため、PascalCase にしておくと camelCase なローカル変数と区別しやすくなる。

今回の問題を踏まえて書き方を改めるべきか迷ったけれど、これは稀なケースだと思うので基本は現状維持するつもり。