かれこれTypeScript+Jest案件で1週間以上ハマっている。それはおそらく各技術をちゃんと理解せず、雰囲気だけで使っているから。
なので私が今良くわかっていない以下の6つの技術に関して、自分なりに整理してみようと思います。
- JavaScript
- 以下予定
- TypeScript
- webpack
- Node.js
- Jest
- babel
JavaScript(ECMAScript)
私のよく知ってるJavascriptは、ブラウザで動くやつです。index.html
に直接書いたり、外部ファイルに記述してscript
タグで読み込んだりするやつです。これについて調べてみます。
もともとは1995年に"make web pages alive"というスローガンのもとに作られ、その目論見は見事成功しているように思えます。もともとはLiveScript
という名前だったようです。それが、当時流行っていたJavaに乗っかる形でJavaScriptと改名したとのことで、Javaと全く無関係ではないようです。しかし1997年にはECMAScript
という名前になり汎用言語としての規格が制定され、JavaScriptはその実装の一つとして独自の進化を遂げ、Javaとは何ら関係なくなりました。
ECMAScriptのバージョンについて
2015年以降は、ECMAScript2015 など、西暦に基づいた名前になっています。以下、主なECMAScriptのバージョンについて書いてみます。
ECMAScript1 (1997)
最初のエディションです。
ECMAScript3 (1999)
- 正規表現
-
try
/catch
が追加されました。ES3は全てのブラウザがサポートする最も高いバージョンです。 トランスパイラのターゲット言語として指定されているのを見たことがありますが、そういうことだったんですね。
ECMAScript5 (2009)
いわゆるES5
です。ES5から追加された機能としては、
"use strict"
-
Array.isArray
やArray.forEach
などの配列周りの関数 JSON.parse
Date.now()
- getterとsetter
- 配列の最後の要素の後の余分なカンマ(←地味に嬉しい)
などがあるようです。だいぶモダンみが増してきました。ES5は全てのモダンなブラウザがサポートする最も高いバージョンです。というのは、IE9が"use strict"
に対応していない、というのがあるみたいです。
ECMAScript6 (2015)
いわゆるES6
ですね。このバージョンから、バージョン番号は西暦の下1桁+1になっててややこしいです。
-
let
とconst
- Promise
- Arrow Functions (
() => {}
みたいなやつ) - Class
- デフォルトパラメータ
などが追加され、より関数型言語を意識した形になっています。個人的にはES5->ES6の変化が最も大きく感じます。IE以外は全てサポートしているようです。
ECMAScript7 (2016)
-
**
(exponential operator)
など。ES7をフルサポートするのは Chrome と Opera のみのようです。
ECMAScript8 (2017)
-
async
/await
構文糖衣ではありますが、これがないと生きていけない人もいるのではないでしょうか。
ECMAScript9 (2018)
- rest / spread properties
- 多値をシンプルに受け渡しできる構文。便利。
環境
ECMAScriptそのものは、I/OやDOMについての規定はないようです。そのへんはJavaScriptにおいて規定されているという認識です。また、JavaScriptといってもブラウザとNode.jsでは環境が異なるはずです。そのへんはまた後で詳しく調べる。
JavaScript Engine
一口にブラウザのJavaScriptといっても、その実装はブラウザによってまちまちのようです。
- Chorome: V8
- FireFox: SpiderMonkey
- IE: Chakra
これらが共通でフルサポートしているのがES5で、後述するトランスパイラはより上位のECMAScriptのバージョンをES5まで落とすことでマルチブラウザ対応を可能にします。
Transpiling
先述したES6以降に備わっている素敵な機能は、全ての環境、ブラウザで使えるわけではありません。では単なる絵に描いた餅かというとそうではなく、ES2015以降で頻繁に行われるようになったトランスパイラなるものを使い、ES3などのどのブラウザでもサポートされているようなバージョンでも動くソースに変換します。
コンパイルは高級な言語からより低級な言語に変換する処理と認識していますが、トランスパイルは、同レベルの別の言語、または同一言語のより下位のバージョンに変換する処理といったところでしょうか。コンパイラはないと困るが、トランスパイラはより幸せになるためのもので、必要性はコンパイラほどではないイメージです。機械語を直で書く人もいるようですが…。
バージョン間の差異を埋めるのにPolyfillという手法もあるようですが、これは実行時にAPIなどの機能を補完するもので、根本的な文法の違いを吸収することはできません。いっそソースを書き換えてしまえというのがTranspilingの考え方かと思います。
例えばトランスパイラは、以下のものをES5やES3に変換します
- TypeScript
- CoffeeScript
- ES2015
ではトランスパイラにはどういうものがあるのかといえば、多分色々あるんでしょうけど、ここでは私の関心のあるところの Babel に限定していろいろ調べてみます。
Babel
Babelのページに行ったら、Babelはコンパイラです、とのことでした。コンパイラはトランスパイラも含むってことですかね。
Plugin
Babelそのものは、プラグインなしでは何もしないようです。全ての変換の仕事はプラグインを追加して初めて行われるようです。No plugin, No transpile です。
Pluginには、Transform PluginsとSyntax Pluginsがあるようです。
Transform Plugins
コードの変換をします。arrow-functions や block-scoping, for-of など、機能ごとに細かく分かれているのがわかります。
ES2015対応などの主だった機能の他、モジュール作法の吸収、実験的な機能、ミニフィケーションなどもTransform Plugins に含まれています。React関連のプラグインがあるのが不思議です。Reactそのものは言語ではないはずですが…。この辺は余裕があれば掘り下げたい。
Tranform Plugins は関連する Syntax Plugin を読み込むので、両方指定する必要はないとのこと。
これらのいわば"部品"を必要に応じて個々に組み合わせることもできるのでしょうけど、我々はそんなことした覚えはありません。普通は後述する Preset を使います。
Syntax Plugins
Syntax Plugins はコードの変換をしないそうです。じゃあ何をするの?→パースをします。と言われてもピンとこない。Syntax Plugins はコードをASTに変換して、Tranform Plugins はASTを操作するのだろうか。この辺は私の関心と外れるので深追いはしない。
Presets
プラグインおまかせ詰め合わせセットだと思います。@babel/preset-env
とかいうやつですね。調べていくと、どうやらただの詰め合わせではないようです(そっちの方が話は単純なのですが…)
@babel/preset-env
よく見るこの人について少し調べてみました。Presets は、オプションを取ることができます。例えばこの人に関して言えば、targets というオプションによって、ターゲットとなるブラウザを絞ることができるようです(知らなかった)。それによる効果はよくわかりませんが、大事なのは、オプションによってプラグイン(詰め合わせ)の内容が変化するということですかね。しかしPresets の挙動を変える要因はこれだけではありません。
このPreset においては、targets が指定されていないときに、browserslist というホストの環境をチェックし、挙動を変えるようなのです。ここからわかることは、Babel の挙動は、Babelの設定ファイルだけでは決まらないということです。JavaScriptを取り巻くソフトウェアたちの設定ファイルの肥大化と偏在化、環境への依存の不透明度が全貌の把握を困難にしているというのが問題だと思っているので、これも元凶の一つかなという感じです。
Configuration
Babelの設定ファイルですが、
-
babel.config.json
またはpackage.json
のbabel
セクション-
node_modules
をコンパイルするとき
-
-
.babelrc.json
- プロジェクトの一部(のディレクトリ?)に適用したいとき
などがあるようです。
これらのファイルは.json
のかわりに.js
や.mjs
、.cjs
として動的に構成することもできる他、.babelrc
は.babelrc.json
の代わりになるというルールもあります。
これらの使い方でけっこうハマりどころがありそうなので以下のリンクをあとで読む(よくわらからずに使っているbabel-jest
についても書いてある…)
https://babeljs.io/docs/en/config-files
ここらで今のところ最も不可解なJestとbabel-jestについて調べてみよう…
Jest
"JavaScript Testing Framework" とのことです。
シンプルさに特化している、設定いらずと公式トップで謳ってますが、使ってみた印象としてはとてもそうは思えませんね。jQueryを使ったレガシーWEBアプリをJestでテストを書きつつwebpack+TypeScriptに置き換えていくという私のユースケースがいけなかったんでしょうけど。おそらく生のJSでNode.jsのコードのテストを書く人は一番幸せに使えるようになっているはずです。
テストフレームワークとのことですが、テストファイルを指定するとテストが走ることから、まずテストランナーとして見ていきます。
テストランナーという存在、苦手なんですよね。いったいどういう環境でテストコードが走っているのか基本的に不透明なので、本番との差異が生まれてもテストランナーのお気持ちがわからず途方に暮れるということが多いです。一方で、そんなことで悩むようなコードはそもそもポータビリティに欠けるといえばそれまでなんですが、既存のレガシーコードに対して段階的にリファクタリング・機能追加していくための初期段階としてのテストは、なるべくレガシーコードを崩さずにやろうとするとどうしてもトリッキーになってしまい、テストフレームワークのお気持ちの問題が浮上してくることが多い気がします。
横道に逸れましたが、例にもれずJestともまだ仲良くなれていません。そもそもそれがこの記事を書く動機であったのであり、根本的な私の理解が弱いというのが主軸だったのでした。文句言わずにいきましょう。
Jest+Babel
https://jestjs.io/docs/en/getting-started#using-babel
公式ドキュメントの1ページ目に、このパターンが解説されています。まずもうこの段階でテンション下がります。Babelを使うには、依存関係をインストールしてくださいとのこと。
Babelを使うというのは、テスト対象のコードがBabelを使っているということでいいんでしょうか。
yarn add --dev babel-jest @babel/core @babel/preset-env
Babelを使ってる人なら、@babel/core
と@babel/preset-env
は入っているんでしょうから、babel-jest
が肝なんだと思いますが、@babel/core
はともかく@babel/preset-env
は本当に必要なんでしょうか?
Configure Babel to target your current version of Node by creating a babel.config.js file in the root of your project:"
まず、すでに babel.config.js
があるんですが、そのままでいいんでしょうか…となります。わざわざ例まで載せてありますが、Jestを使うにあたり、なにか重要な点が書かれているとは思えず、私は混乱するばかりです。
さらに次の備考で混乱します
Note: babel-jest is automatically installed when installing Jest and will automatically transform files if a babel configuration exists in your project. To avoid this behavior, you can explicitly reset the transform configuration option:
依存モジュールのインストール含め、ここまでの説明いらなかったのでは?となります。Jestはともかく、このドキュメントを書いた人に対してだんだん心配になってきます。
とりあえず調べて現時点でわかったこととして、Jestの挙動はjest.config.js
などの設定ファイルおよびコマンドラインオプションで制御されていること、そしてBabelとの連携は、初期設定では
transform: {"^.+\\.[jt]sx?$": "babel-jest"}
という設定項目によって実現されていそうなことがわかりました。
babel-jest
いよいよこの得体の知れない、核心に迫っていきましょう。どうやらこの人はJestリポジトリのサブパッケージとして存在しているようです。Jestの子会社みたいなものかな?
ソースが短そうなので読んでみましたが…私の現時点の知識ではちょっと難しかったです。BabelのTransform Plugin として書かれてるのかな?と予想。
そもそもこのtransform
という設定項目は何なのでしょうか。私の調べたところでは、テスト対象のコードを変換するためのプラグインを指定するところ、です。ファイルのパスのパターンごとにトランスフォーマーを指定できます。
ここが悩ましいポイントではないでしょうか。テストは大事ですが、基本的にはコードはテストがなくても動くものであり、当然ながらコードをコンパイルするための環境は別途構築してあるものです。にもかかわらず、Jestはその環境をリスペクトはしてくれないようです。
TODO: Babel についてよく理解してから続きを書く
webpack+Jest
私は上記のページを何度かななめ読みしたのですが、何もわかりませんでした。なぜだろう?と今一度しっかり読み直して、理由がわかりました。
Using with webpack と謳っているので、webpack と連携してくれるんだ!さすが!と思い込んでいて、実際のプロジェクトでもなんとなくwebpack が仕事してるような気がしてたんですが、どうやら Jestはwebpackと何ら連携しません! 嘘だったらごめんなさい。
それはこの一文から明らかになりました。
Let's start with a common sort of webpack config file and translate it to a Jest setup.
このwebpackの設定ファイルを例に、Jestの設定に翻訳しようぜって書いてあったんです。webpackと同等の設定ができるよ!でもできないこともあるかもだから、そのときはなんか便利なモジュールとかあるからがんばってね!的なことが書いてありました。
最後の一行に"React と連携した複雑な例はこちら"とあったので、あまり期待しないで見てみたら、なんとjest.config.js
が使われていませんでした。
かろうじてJestに指示を与えてそうなのはpackage.json
にある2項目のみ。もはやBabelと連携しているかどうかすら不透明です。設定のデフォルト値のことを考えると、流石にBabelとは連携してそうだな…とpackage.json
を再び見るも、babel-jest
は依存モジュールに含まれていない…。Jestをインストールするとbabel-jestも入るというのは本当なのか…?それとも実はやはりwebpackとちゃっかり連携しちゃっているのか… おそらく前者だろうが、やはり手を動かさないと本当のところはわからなそうだ…。
jest_react_redux_node_webpack_complex_example
強そうな名前です。戦ってみます。Github Desktopでダウンロードし、VSCodeで開きました。おもむろにRunボタンを押して、create a launch.json file リンクからChrome(preview)を選択してみました。意味はよくわかっていません。なにやら設定ファイルができたので、Launch Chrome against localhost ボタンを押してみます。しかし残念ながらこれだけでは実行することができませんでした。どうやらこの設定はただ8080番ポートでディレクトリをサーブするだけみたい。
次に、Node.js(preview)の方を選んでみます。こちらは特に設定ファイルを作らなくてもメニューに存在していました。どうやらpackage.jsonのrunスクリプトを実行してくれるみたい?とりあえずstartを選択してStart debugging。すると、node_modulesがないよと怒られました。それはやってくれないのね、ということで
npm install
すると、
added 1000 packages from 539 contributors and audited 1074 packages in 85.027s
found 1999 vulnerabilities (999 low, 3 moderate, 996 high, 1 critical)
run `npm audit fix` to fix them, or `npm audit` for details
とのことです。数字きりが良すぎるけどマジか?
とりあえず言われるがままに
npm audit fix
しました。すると
fixed 1929 of 1999 vulnerabilities in 1074 scanned packages
2 package updates for 70 vulnerabilities involved breaking changes
(use `npm audit fix --force` to install breaking changes; or refer to `npm audit` for steps to fix these manually)
1929の脆弱性を修正したが、まだ70残っていると。本当か?やり方あってるのかな?
npm audit fix --force
おそらくこのリポジトリ自体が最終更新3年前というのが原因でしょうね。
うまくいったようなので、Start debugging ボタンを再び押します。要は
npm run start
です。なにやらターミナルに色々表示されています。
$ npm run start
Debugger listening on ws://127.0.0.1:49792/f47ca75a-788c-4c96-b387-b785e6a3efb3
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
> Example@1.0.0 start /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example
> npm-run-all --parallel watch:server watch:build
Debugger listening on ws://127.0.0.1:49807/b392e031-1a68-4433-a89c-b5debe506930
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
Debugger listening on ws://127.0.0.1:49816/1068281b-b179-4d3b-86e0-e32cc20f6ce0
For help, see: https://nodejs.org/en/docs/inspector
Debugger listening on ws://127.0.0.1:49817/ec448529-2caa-414b-9eae-d835dfcaf663
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
Debugger attached.
> Example@1.0.0 watch:server /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example
> nodemon "server/server.js" --watch "./server"
> Example@1.0.0 watch:build /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example
> webpack -d --watch --progress --colors
Debugger listening on ws://127.0.0.1:49832/01011404-2ade-4bb0-a267-4e5874b95d01
For help, see: https://nodejs.org/en/docs/inspector
Debugger listening on ws://127.0.0.1:49833/1a3daa1d-f66f-41ac-832c-1a3e4dc66a33
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
Debugger attached.
One CLI for webpack must be installed. These are recommended choices, delivered as separate packages:
- webpack-cli (https://github.com/webpack/webpack-cli)
The original webpack full-featured CLI.
We will use "npm" to install the CLI via "npm install -D".
Do you want to install 'webpack-cli' (yes/no): [nodemon] 1.18.3
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example/server/**/*
[nodemon] starting `node server/server.js`
Debugger listening on ws://127.0.0.1:49849/68aedd18-9d69-437c-b7a2-0e24f463faae
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
Server is listening to port : 5000
Do you want to install 'webpack-cli' (yes/no):
本当はもっとカラフルです。やたらデバッガが走っているようです。そもそもnpm run start
で何が動いていたかというと、
npm-run-all --parallel watch:server watch:build
です。parallelなので出力が入り乱れているようですね。何やらwebpack-cli
をインストールしますかと聞かれているので、入れてもらうことにします。…と思いきや、このターミナルでは文字入力を受け付けていないみたいです…。
いきなりVSCodeという未知な挙動の多い環境で動かしたのがよくなかった。素直にコンソールからいきます。コンソールを開いて改めて
npm run start
しかし"yes"が入力できないことに変わりはありませんでした。2つのプロセスが走っていることが原因か?
ちょっと方向を変えて、
npm test
からやってみます。そもそもjestの挙動を知りたいんだった。
$ npm test
> Example@1.0.0 test /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example
> jest
FAIL reactTests/tests/teamAmerica.test.js
● Test suite failed to run
Plugin/Preset files are not allowed to export objects, only functions. In /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example/node_modules/babel-preset-es2015/lib/index.js
at createDescriptor (node_modules/@babel/core/lib/config/config-descriptors.js:178:11)
at node_modules/@babel/core/lib/config/config-descriptors.js:109:50
at Array.map (<anonymous>)
at createDescriptors (node_modules/@babel/core/lib/config/config-descriptors.js:109:29)
at createPresetDescriptors (node_modules/@babel/core/lib/config/config-descriptors.js:101:10)
at presets (node_modules/@babel/core/lib/config/config-descriptors.js:47:19)
at mergeChainOpts (node_modules/@babel/core/lib/config/config-chain.js:384:26)
at node_modules/@babel/core/lib/config/config-chain.js:347:7
at buildRootChain (node_modules/@babel/core/lib/config/config-chain.js:129:29)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 3.993 s
Ran all test suites.
npm ERR! Test failed. See above for more details.
いきなり失敗しました。さすが複雑な例!まず考えさせるのだな。どうもBabelのプリセットファイルから発せられているようです。ソースコードを読むことを考えましたが、まずはググってみます。
この辺が怪しいですね。なにせ3年前のコードですし、先程脆弱性がどうのこうのでビルドソフトウェアだけはバージョンアップされてそうですから。
Babelの設定は.babelrc
にありそうです。
{
"presets": [
["env", { "modules": false }],
"es2015",
"react",
"stage-2"
],
"plugins": ["transform-class-properties"]
}
ふむ。先程のスレッドにあったように、@babel
が要るのではないか?そもそもこれなんのためにつけるの?
調べていくと、babel-preset-env
というnpmモジュールもあり、Babelのプリセットとして指定するときは、単にenv
と書くようです。これはBabel6.26.3のドキュメントに載っていました。
また、こういう記事もありました
Yearly Preset (babel-preset-es2015
など)は使わず、babel-preset-env
使えとのこと。いろいろ歴史があるんだなぁ。
また@babel
というのは、Scoped Package という記法らしい。よくわからんが、Babel7からこれになっていると仮定する。これらを踏まえて、以下のようにしてみました。
{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react",
],
"plugins": ["transform-class-properties"]
}
これらのプリセットをインストールします。
npm i -D @babel/preset-env @babel/preset-react
監査も終えて、いざテスト
$ npm test
> Example@1.0.0 test /Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example
> jest
FAIL reactTests/tests/teamAmerica.test.js
● Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
/Users/toshi/Documents/GitHub/jest_react_redux_node_webpack_complex_example/reactTests/tests/teamAmerica.test.js:1
import React from 'react';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Runtime._execModule (node_modules/jest-runtime/build/index.js:1179:56)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 4.485 s
Ran all test suites.
npm ERR! Test failed. See above for more details.
で、でたー!
そもそもnpm test
はここではjest
を実行するのみだったので、これはまんまjestによって実行されていたのでした。ここまででわかったことは
- jestはBabelの設定を見に行っていること
- babel-jestが本当にいつの間にかインストールされていたこと
さて、長くなってきたのでさっさと終わらせましょう。
まずはエラーに注目します。
SyntaxError: Cannot use import statement outside a module
なぜ公式のサンプルにこんなエラーが残っているのでしょう?おそらく3年前のコードだからでしょう。jestはBabelの設定を見ているはずなので、Babelの設定をもう一度見てみましょう。
{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react",
],
"plugins": ["transform-class-properties"]
}
プリセットとして@babel/preset-env
を指定しています。まずこの人の仕様を知る必要がありそうです…。
色々気になることはあるのですが、まずは{ "modules": false }
について調べましょう。これはプリセットに対するオプションです。
Setting this to false will preserve ES modules. Use this only if you intend to ship native ES Modules to browsers.
とのことですが、まず ES modules がわかりません。調べてみると…なんとimport
文はモダンなブラウザでは使えるようになっていたらしい。言わばブラウザネイティブなモジュールシステムですかね。ではなんでimport
文でエラーになったんでしょう。よく読むと、モジュールの外では使えないと書いてありますので、このスクリプト自体がモジュールと認識されていないようです。
では何だと認識されているのでしょう。これを紐解くには、package.json
についても知る必要がありそうです。Babel configのページを見ると、明らかにBabelはpackage.json
の存在およびその内容に依存しているからです。というか、Node.jsの挙動か…。Node.jsについて詳しく調べないまま進んでいるのでうまく整理できないな。
具体的には、package.json
に"type": "module"
の記述があると、Node.js v.12以降は拡張子.js
を ES Modules として扱うとのこと。このサンプルプロジェクトではこの記述はなく、私が使用しているNode.jsはv12.18.2であったことから、テストファイルteamAmerica.test.js
はCommonJS形式と認識されていたと推測されます。
しかしさらにいろいろ調べているうち、package.json
で指定されているBabelのバージョンが6だったことに気づき、前提が崩れてきた…
時間がないので今日はここまで。
TODO: 今度こそbabel-jestの動きを探る
参考
https://www.w3schools.com/js/js_versions.asp
https://javascript.info/intro
https://en.wikipedia.org/wiki/ECMAScript
https://babeljs.io/
https://jestjs.io/en/