TypeScript( or JavaScript ) + mocha + power-assert + espower-typescript 注意点まとめ

espower-typescript index.js diff

TypeScript( or JavaScript ) + mocha + power-assert + espower-typescript の環境構築時に結構はまったので、需要あるかわからないですが、注意点をまとめてみました。

以下、TyepScript はTS、JavaScript はJSと略します。

espower-typescript の PR #28 Transpile .js files as well if allowJs:true exists in tsconfig.json を見て、そういえば前にソース修正して対応したなと思い出し、こちらについても最後に記載しておきます。

PR していないのは、トランスパイル処理をまるっと変更していて、副作用が無いか typescript-simple について調査してからにするか、と思ったまま忘れていたためです…

この PR を見て需要あるかなと書いている訳ですが、現在は mocah よりも ava で環境構築することおすすめしています。ava は並列処理してくれるので高速です。power-assert が内部で呼ばれているので、エラー失敗時の情報も十分です。

上記を踏まえて、以下本題です。

espower-typescript とは?

TS でも power-assert を使用できるようにするパッケージです。
power-assert を使用すると、お決まりの assert を使用してテストを書いても、テスト失敗時にわかりやすいエラー情報が得られます。
つまり、TS でもテスト書こうねと言いやすくなる。

espower-typescript 使用時の注意点

本題ですが、espower-typescript はデフォルトで想定されている環境とマッチしていないと、power-assert を使用できません。

注意点1: test ディレクトリ

Zero-config mode では、カレントディレクトリにtestディレクトリが配置されており、その下にテスト用の.tsファイルが配置されていることを想定しています。

GitHubのREADMEにも書かれていますが、テストを配置しているディレクトリがカレントディレクトリ直下のtestではない場合、package.json に下記のようにして知らせる必要があります。

"directories": {"test": "your/test/dir/here"}  

ソースを確認すると、カレントディレクトリのパスと minimatch のパターンが前後に追加されるため、./ などは使用しない方が良さそうです。

注意点2: tsconfig.json の読み込み

カレントディレクトリに tsconfig.json を配置することで、compilerOptions の設定内容でトランスパイルしてくれます。
読み込まれるのは compilerOptions のみで、files や include、exclude は読み込まれません。(テストファイルを exclude していても安心)

TS では tsconfig.json のある場所がプロジェクトルートとして扱われます。tsconfig.json は複数配置しても、tsc に --project オプションでディレクトリを指定することでトランスパイルすることができます。
しかし、espower-typescript はカレントディレクトリ直下のみサポートしているので、プロジェクトルートが複数ある場合は、テスト実施前に、カレントディレクトリにテスト用 tsconfig.json を作成する必要があります。

tsconfig.json が見つからない場合、デフォルト設定でトランスパイルされるようです。

注意点3: assertのインポート文

power-assert を使う場合は assert としてモジュールをインポートすることが重要で、トランスパイルされたJSが下記のいずれかになる必要があります。別名でインポートされると通常の assert として扱われ、テスト失敗時に power-assert の詳細情報が出力されません。

var assert = require('power-assert');

または

var assert = require('assert');

2つ目の 'assert' で指定する場合は、@types/node を追加して、TS にモジュールを解釈させます。

Avoid

import assert from 'power-assert';

ES2015(ES6) で power-assert を使う例としてよく目にするこの書き方は、TS が下記のようにトランスパイルしてしまいます。(Babelでは問題ないんだけどね)

var power_assert_1 = require('power-assert');

power_assert_1 という別名になり、通常の assert として扱われ、テスト失敗時に power-assert の詳細情報が出力されません。

Use

import * as assert from 'power-assert';

または

const assert = require('power-assert');

上記のようにすることで、モジュールが全て読み込まれ、TS でも正しくトランスパイルしてくれます。
JS で上記のように書いて、Babel でトランスパイルしても問題ないので、なるべくこの書き方で統一しましょう。

構築環境

個人的に、対応するテストファイルが存在しているか分かりやすいので、以下のようなディレクトリ構造で環境構築しています。

ディレクトリ構造

root
│  package.json
│  tsconfig.json
│
├─dist
│      hoge.js
│
├─node_modules
└─src
        hoge.test.ts
        hoge.ts

pacage.json

{
  "name": "testenv",
  "version": "1.0.0",
  "scripts": {
    "start": "npm run clean && npm run build && npm test",
    "clean": "rimraf ./dist",
    "build": "tsc --outDir ./dist",
    "test": "mocha  --timeout 50000 --compilers ts:espower-typescript/guess ./src/**/*.test.*"
  },
  "files": [
    "dist"
  ],
  "directories": {
    "test": "src/"
  },
  "devDependencies": {
    "@types/mocha": "^2.2.41",
    "@types/node": "^7.0.22",
    "espower-typescript": "^8.0.0",
    "mocha": "^3.4.1",
    "power-assert": "^1.4.2",
    "rimraf": "^2.6.1",
    "typescript": "^2.3.2"
  }
}

tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs"
    },
    "include": [
        "./src"
    ],
    "exclude": [
        "./**/*.test.*"
    ]
}

もう一歩踏み込んで AllowJs

tsc は ES2015(ES6) から ES5 などの、JStoJS トランスパイルも可能です。

  • tsc が生成する JS が読みやすい
  • 訳あって .ts と .js が混在している

などの理由で、tsc を使いたいってことがあったりします。
そんな時は "allowJs": true オプションを追加することで JS を扱えます。

tsconfig.json

{
    "compilerOptions": {
        "allowJs": true,
        "checkJs": true,
        "target": "es5",
        "module": "commonjs",
        "lib": [
            "es2015",
            "dom",
            "scripthost"
        ]
    },
    "include": [
        "./src"
    ],
    "exclude": [
        "./**/*.test.*"
    ]
}

こんな感じにしておくと tsc がうまくトランスパイルしてくれます。

では、test はどうでしょうか?
ES2015(ES6) で記述された JS は、冒頭で紹介した espower-typescript の PR #28 にもあるように、エラーが発生してしまいます。
espower-typescript のソースを見ると .ts と .tsx のみ require.extensions[] しているようで、.js はトランスパイルされません。

テスト用に Babel 環境を追加するのもちょっとなぁと思い、関連ソースを修正して対応することにしました。
espower-typescript の guess.js と index.js を以下のように修正しています。

node_modules/espower-typescript/esguess.js

--- a/guess.js
+++ b/guess.js
@@ -3,18 +3,21 @@ var path = require('path');

 var ts = require('typescript');

-var pattern = 'test/**/*.ts';
 var cwd = process.cwd();
 var packageData = require(path.join(cwd, 'package.json'));

+var testDir = 'test';
 if (packageData &&
   typeof packageData.directories === 'object' &&
   typeof packageData.directories.test === 'string') {
-  var testDir = packageData.directories.test;
-  pattern = testDir + ((testDir.lastIndexOf('/', 0) === 0) ? '' : '/') + '**/*.ts';
+  testDir = packageData.directories.test;
 }
+var pattern = testDir + ((testDir.lastIndexOf('/', 0) === 0) ? '' : '/') + '**/*.ts';

 var tsconfigPath = ts.findConfigFile(cwd, fs.existsSync);
+if (tsconfigPath === undefined) {
+  tsconfigPath = ts.findConfigFile(testDir, fs.existsSync);
+}
 var tsconfigBasepath = null;
 var compilerOptions = null;
 if (tsconfigPath) {

node_modules/espower-typescript/index.js

--- a/index.js
+++ b/index.js
@@ -16,13 +16,19 @@ function espowerTypeScript(options) {
   var tss = new TypeScriptSimple(compilerOptions, false);

   function loadTypeScript(localModule, filepath) {
-    var result = tss.compile(fs.readFileSync(filepath, 'utf-8'), path.relative(cwd, filepath));
+    var source = fs.readFileSync(filepath, 'utf-8').toString();
+    var result = ts.transpileModule(source, compilerOptions).outputText;
+    // var result = tss.compile(fs.readFileSync(filepath, 'utf-8'), path.relative(cwd, filepath));
     if (minimatch(filepath, pattern)) {
       result = espowerSource(result, filepath, options);
     }
     localModule._compile(result, filepath);
   };

+  if (compilerOptions.allowJs) {
+    pattern = pattern.replace(/\*\.ts$/, '{*.ts,*.js}');
+    require.extensions['.js'] = loadTypeScript;
+  }
   require.extensions['.ts'] = loadTypeScript;
   require.extensions['.tsx'] = loadTypeScript;
 }

JS対応の修正内容は以下になります。

  • AlloJs のコンパイルオプションが true のときに、minimatch 用のパターンに .js を追加
  • AlloJs のコンパイルオプションが true のときに、require.extensions[‘.js’] で JS も拡張読み込み
  • typescript-simple を使用せずに、typescript モジュールの transpileModule でトランスパイル

最後の修正は typescript-simple に JS を流し込むと例外が発生してしまったためです。ソースを見たところ、トランスパイル結果がテキストで得られれば良さそうだったので、上記のように修正しています。(副作用あるかも。参考にして修正する場合は自己責任でお願いします)

修正内容には tsconfig.json がカレントディレクトリで見つからない場合に、テストディレクトリを探す処理が含まれています。(guess.js の最後の追加箇所)
複数の tsconfig.json を使用したい場合や、以下のディレクトリ構造に対応するためです。

ディレクトリ構造

root
│  package.json
│
├─dist
│  ├─css
│  └─js
├─node_modules
└─src
    ├─raw
    ├─scss
    └─ts
            tsconfig.json

このディレクトリ構造を使用する場合は、tsc に –project オプションを与えて tsconfig.json の場所を知らせます。

"build": "tsc --project ./src/ts --outDir ./dist/js"

また、tsconfig.json の files や include、exclude はこのプロジェクトルートからの相対パスになるので、注意しましょう。

まとめ

espower-typescript を使うときは以下に注意しよう。

  • test ディレクトリが ./test でない場合、pacage.json に directories フィールドで指定する
  • tsconfig.json はカレントディレクトリにある場合に読み込まれる
  • assertのインポート文は import * as assert from 'power-assert'; を使用する
  • espower-typescript でも JStoJS トランスパイルしたい場合はソースを修正する(自己責任で)