TypeScriptを約5年ぶりにやってみたら、ほとんど何も覚えていないし、色々変わっていたので再挑戦

正確な時期を覚えていないのですが、2012年から2013年ごろにTypeScriptを書いていました。TypeScriptがまだ正式リリースされていなくて、アップデートの度にコンパイルできなくなったり、警告が出たり、そういうことをしていた記憶があります。ずっとフロントエンドからは離れてPythonばかり書いていました。久しぶりにTypeScriptをしっかりと書いてみようと思ったら、TypeScriptのバージョンが2.6.1になっていました。

曖昧な記憶ですが、当時はGruntを使ってMochaやJasmineを使ってテストをPhantomJS上で走らせていました。JavaScriptの世界は新しいツールの登場のサイクルがとても早いので、一からやり直そうと思い挑戦しました。ステップバイステップなので、同じファイルを何度も編集したり、一度で済ませられるコンテナの操作を何度も行って非効率ですが、私と同じ立場の人、未来の自分には、なぜそうしたのかが分かりやすいのではないかと考えて、そのように書いています。

Vagrant上のUbuntu 16.04.3でやりました。 docker.iodocker-compose をapt経由で仮想マシンにインストールしてあります。

今回のプロジェクトの名前は hellotsagain です。

Dockerを準備

会社ではDockerは別の人が担当していますし、個人でやるときは何となくで使っていたので、まずはしっかりとDockerの準備をするところから始めます。

まずは Dockerfile を作ります。

FROM node:9.0.0

WORKDIR /srv

とりあえず最初は特にDockerfileに書くことがありませんが、すぐにいくつか書くことが出てくるので、とりあえずファイルを作ってしまいます。

次に docker-compose.yml を作ります。docker-composeを使ってコンテナの管理をします。

hellotsagain:
  build: .
  command: echo Hello World

コンテナを起動した時に command の内容がコンテナ内で実行されます。

$ sudo docker-compose build hellotsagain
$ sudo docker-compose run --rm hellotsagai
Hello World

コンテナをビルドして、実行します。 run--rm を与えることで、終了したコンテナを残さないようにします。

$ sudo docker-compose run --rm hellotsagain node --version
v9.0.0

run の実行コンテナの後に、実行したいコマンドを書けば docker-compose.yml に書かれた command を上書きして実行できます。

これでDockerの準備は整いました。

パッケージマネージャ

かつてはnpmを使っていましたが、調べてみるとyarnというのが後発で出てきていました。 Facebookのポスト によると、登場は2016年10月ごろのようです。npmはとても遅かった記憶があるので、yarnがnpmと比べて高速という部分にとても魅力を感じます。というわけでyarnを使います。

yarnはnpmと同じ package.json を使います。yarnが依存パッケージをこの中に書いていってくれるので、Dockerの中と外でこのファイルを共有したいです。そのために docker-compose.yml を編集します。

hellotsagain:
  build: .
  command: echo Hello World
  volumes:
    - .:/srv

追記したのは volumes の部分です。これでDockerfileやdocker-compose.ymlがあるディレクトリがDockerコンテナの /srv にマウントされます。Dockerコンテナは /srv を作業ディレクトリにするように指定してありますし、package.jsonは自然とこの共有されたディレクトリに作られます。

Dockerコンテナを起動してbashを起動します。これでコンテナ内で自由にコマンドを実行できます。

$ sudo docker-compose run --rm hellotsagain ls
Dockerfile  docker-compose.yml

マウントしているおかげで、コンテナ内で ls を実行すると、Dockerfileとdocker-compose.ymlがあることが確認できます。

$ sudo docker-compose run --rm hellotsagain yarn init
yarn init v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
question name (srv): hellotsagain
question version (1.0.0): 0.0.0
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
Done in 12.10s.

yarnがnode 9.0.0をサポートしていないという警告が出ていますが、コンテナに元々インストールされていることと、今のところ問題が起きていないので、今回はこのまま進みます。

yarn init により、 package.json が作られました。コンテナ内で作られましたが、ディレクトリをマウントしているので、ホストからも見えます。

$ cat package.json
{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT"
}

TypeScriptをインストールしておく

まずは手動で

yarnを使ってglobalにインストールします。

$ sudo docker-compose run --rm hellotsagain bash
# yarn global add typescript
yarn global v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
warning Your current version of Yarn is out of date. The latest version is "1.3.2" while you're on "1.2.1".
info To upgrade, run the following command:
$ curl -o- -L https://yarnpkg.com/install.sh | bash
success Installed "typescript@2.6.1" with binaries:
      - tsc
      - tsserver
Done in 1.50s.
# tsc --version
Version 2.6.1

一度コンテナの中で手動でインストールしてみます。 yarn global add がグローバルにインストールするためのコマンドです。コマンドがパスの通った場所にインストールされます。

次は自動化

問題なくインストールできたので、次はコンテナをビルドした時にインストールされるようにします。

Dockerfileに RUN yarn global add typescript を追記します。

FROM node:9.0.0

WORKDIR /srv

RUN yarn global add typescript

全体はこうなりました。次にビルドして実行してみます。

$ sudo docker-compose build
Building hellotsagain
Step 1 : FROM node:9.0.0
 ---> 9c60c5cb89d2
Step 2 : WORKDIR /srv
 ---> Running in 370370898eb4
 ---> d2ce45b35680
Removing intermediate container 370370898eb4
Step 3 : RUN yarn global add typescript
 ---> Running in b867466f6d66
yarn global v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "typescript@2.6.1" with binaries:
      - tsc
      - tsserver
Done in 1.69s.
 ---> 11abdfa83c95
Removing intermediate container b867466f6d66
Successfully built 11abdfa83c95
$ sudo docker-compose run --rm hellotsagain tsc --version
Version 2.6.1

build時にTypeScriptをインストールすることができました。

TypeScriptをビルドする

ソースコードの用意

ようやくここまできました。まずはTypeScriptを書きます。srcというディレクトリを作り、その中にhellotsagain.tsを作ります。足し算をするだけのメソッドです。

export module math {
    export class Calculator {
        sum(a: number, b: number): number {
            return a + b;
        }
    }
}

これでmathパッケージのCalculatorクラスのsumメソッドができました。次にこれをtscで直接コンパイルします。

tscで直接コンパイル

$ sudo docker-compose run --rm hellotsagain tsc src/hellotsagain.ts
$ cat src/hellotsagain.js
"use strict";
exports.__esModule = true;
var math;
(function (math) {
    var Calculator = /** @class */ (function () {
        function Calculator() {
        }
        Calculator.prototype.sum = function (a, b) {
            return a + b;
        };
        return Calculator;
    }());
    math.Calculator = Calculator;
})(math = exports.math || (exports.math = {}));

JavaScriptが生成されました。成功です。

とりあえずこのJavaScriptはいらないので消しておきます。

$ rm src/hellotsagain.js

TypeScriptをテストする

テストを書く

テストファーストにしたいところですが、再入門でいきなりはハードルが高いので、テストが後になってしまいました。

import { math } from '../src/hellotsagain';

it('足し算', () => {
    const calculator = new math.Calculator();

    expect(calculator.sum(1, 2)).toBe(3);
});

Jestの用意

かつての私はTypeScriptのテストにMochaやJasmineを使っていました。調べてみると、今はJestというのがありました。Facebook製で、JasmineをベースにしているAll in oneのようなテストフレームワークのようです。今回はこれでいきます。

まずは、コンテナに入ってインストールしてみます。

$ sudo docker-compose run --rm hellotsagain bash
# yarn yarn add --dev jest
yarn run v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
error Command "yarn" not found.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
root@9fe1ccaa0d6d:/srv# yarn add --dev jest
root@9fe1ccaa0d6d:/srv# yarn add --dev jest
yarn add v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.3.2" while you're on "1.2.1".
info To upgrade, run the following command:
$ curl -o- -L https://yarnpkg.com/install.sh | bash
success Saved 328 new dependencies.
...

今回は global なし、かつ --dev フラグありでインストールしました。 global なしだとパスが通った場所にコマンドが置かれません。 --dev フラグは、そのモジュールが開発時にのみ必要なものであることを意味しています。

ここでpackage.jsonを確認してみます。

{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^21.2.1"
  }
}

devDependencies が追加されています。yarnはpackage.jsonを読んで依存しているモジュールをすべてインストールする機能があります。この機能を使ってコンテナのビルド時に依存モジュールがすべてインストールされるようにします。Dockerfileに RUN yarn install を追加します。

FROM node:9.0.0

WORKDIR /srv

RUN yarn global add typescript
RUN yarn install

Dockerfile全体はこうなりました。

では、ビルドしてみます。

$ sudo docker-compose build hellotsagain
Building hellotsagain
Step 1 : FROM node:9.0.0
 ---> 9c60c5cb89d2
Step 2 : WORKDIR /srv
 ---> Using cache
 ---> d2ce45b35680
Step 3 : RUN yarn global add typescript
 ---> Using cache
 ---> 70df05602950
Step 4 : RUN yarn install
 ---> Using cache
 ---> d308f8ec578a
Successfully built d308f8ec578a

次に、 $ npm test でjestが実行されるように、package.jsonに scripts を追加します。

{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^21.2.1"
  },
  "scripts": {
    "test": "jest"
  }
}

package.json全体はこうなりました。テストを実行してみます。

$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

No tests found
In /srv
  1 file checked.
  testMatch: **/__tests__/**/*.js?(x),**/?(*.)(spec|test).js?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 1 match
Pattern:  - 0 matches

テストが見つからないとエラーになりました。JestはJavaScript用のテストフレームワークなのでTypeScriptと繋いでやらないといけません。

JestとTypeScriptを繋ぐ

ts-jestというモジュールがJestとTypeScriptの架け橋になってくれます。これまで通り、まずはコンテナの中に入ってインストールしてみます。

$ sudo docker-compose run --rm hellotsagain bash
# yarn add --dev ts-jest
yarn add v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "ts-jest@21.2.1" has unmet peer dependency "typescript@2.x".
[4/4] Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.3.2" while you're on "1.2.1".
info To upgrade, run the following command:
$ curl -o- -L https://yarnpkg.com/install.sh | bash
...

warning "ts-jest@21.2.1" has unmet peer dependency "typescript@2.x". がありますので、TypeScriptもインストールします。

# yarn add --dev typescript
yarn add v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
└─ typescript@2.6.1
Done in 8.56s.

package.jsonにJestをインストールしたときのようにdependenciesが足されています。

{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^21.2.1",
    "ts-jest": "^21.2.1",
    "typescript": "^2.6.1"
  },
  "scripts": {
    "test": "jest"
  }
}

コンテナをビルドし直します。

$ sudo docker-compose build hellotsagain
...

このセクションは長いですね。まだ続きます。

ts-jestをインストールしただけでは、Jestはそれを使ってくれません。

$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

No tests found
In /srv
  1 file checked.
  testMatch: **/__tests__/**/*.js?(x),**/?(*.)(spec|test).js?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 1 match
Pattern:  - 0 matches

package.jsonにjestの項目を追加します。

{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^21.2.1",
    "ts-jest": "^21.2.1",
    "typescript": "^2.6.1"
  },
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "transform": {
      "\\.ts$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
    },
    "testRegex": "\\.test\\.ts$",
    "moduleFileExtensions": ["ts"]
  }
}

package.json全体はこうなりました。

改めてjestを走らせます。

$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

 FAIL  tests/hellotsagain.test.ts
  ● Test suite failed to run

    Cannot find module './create_spy' from 'jasmine_light.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:191:17)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        2.138s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

手強いですね。「テストが見つからない」から「テストを走らせるのに失敗した」なので進んでいます。

Cannot find module './create_spy' from 'jasmine_light.js' に注目します。先程、package.jsonのmoduleFileExtensionsに「ts」しか指定しませんでしたが、依存モジュールはJavaScriptであることがほとんどでしょう。ここに「js」を足して "moduleFileExtensions": ["js", "ts"] にします。

再度テストを走らせます。

$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

 FAIL  tests/hellotsagain.test.ts
  ● Test suite failed to run

    {
        "messageText": "Cannot read file '/tsconfig.json': ENOENT: no such file or directory, open '/tsconfig.json'.",
        "category": 1,
        "code": 5012
    }

      at readCompilerOptions (node_modules/ts-jest/dist/utils.js:73:15)
      at Object.getTSConfig (node_modules/ts-jest/dist/utils.js:142:18)
      at Object.getCacheKey (node_modules/ts-jest/dist/preprocessor.js:43:28)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.623s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

今度は「tsconfig.json」が見つからないと言われました。このファイルはTypeScriptのコンパイル時のオプションを書いておけるファイルですね。この記事の中では先にtscをそのまま使って、特にオプションを使用せずにすべてデフォルトに任せていました。しかしより本格的に開発するのならtsconfig.jsonを使ってコンパイルオプションを指定するでしょうし、テストが必要と言っているので作ります。

$ sudo docker-compose run --rm hellotsagain bash
# tsc --init
message TS6071: Successfully created a tsconfig.json file.

コンテナの中に入るまでもありませんでした。これなら $ sudo docker-compose run --rm hellotsagain tsc --init でも十分でした。

もう一度テストを走らせます。

$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

 PASS  tests/hellotsagain.test.ts
  ✓ 足し算 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.083s
Ran all test suites.

ついにテストに成功しました。

あと少し

テストの型チェックができていないことに気が付きました。さらに言えば、テストはコンパイルされたくありません。

型チェックをtscでのコンパイル時にしかしていなかったので気がつくのが遅れました。エディタに型チェックの支援や、型を使ったサポートをさせるべきでした。とにかく今から解決します。

$ sudo docker-compose run --rm hellotsagain tsc
tests/hellotsagain.test.ts(3,1): error TS2304: Cannot find name 'it'.
tests/hellotsagain.test.ts(6,5): error TS2304: Cannot find name 'expect'.

型チェック

Jestの型定義をインストールします。五年前はたしか専用のツールを使ってインストールしていたと記憶しています。それが随分と楽になったものです。

$ sudo docker-compose run --rm hellotsagain yarn add --dev @types/jest
...
$ sudo docker-compose build hellotsagain
...
$ sudo docker-compose run --rm hellotsagain tsc
node_modules/@types/jest/index.d.ts(1046,34): error TS2304: Cannot find name 'Set'.

このSetはes6から使えるようです。tsconfig.jsonでtargetをes5になっているのが原因です。これをes6にしたら解決しますが、これは es6のサポート状況 と相談して決めないといけません。ここではes6をターゲットにすることにします。

$ sudo docker-compose run --rm hellotsagain tsc

これでテストの型チェックでエラーがでなくなりました。

テストはコンパイルしない

"exclude": ["tests"] をtsconfig.jsonに加えます。

{
  "exclude": ["tests"],
  "compilerOptions": {
    /* Basic Options */
    "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation:  */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true                            /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "./",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "./",                       /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

コンパイルされたJavaScriptを削除してから、もう一度tscでコンパイルしてみます。

$ rm tests/hellotsagain.test.js
$ sudo docker-compose run --rm hellotsagain tsc
$ cat tests/hellotsagain.test.js
cat: tests/hellotsagain.test.js: No such file or directory

ビルドされたJavaScriptをブラウザで実行可能にする

HTMLの用意

package.jsonがあるのと同じディレクトリにindex.htmlを用意します。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>もう一度TypeScript</title>
    </head>
    <body>
        <script src="/src/hellotsagain.js"></script>
    </body>
</html>

PythonでHTTPサーバを起動します。 $ python3 -m http.server 8080 Python3がないのならPython2でもできます。 $ python -m SimpleHTTPServer 8080 ブラウザでアクセスします。

しかしexportが定義されていないとエラーになってしまします。

export is not defined

export is not definedを解決する

これはtsconfig.jsonでmoduleがcommonjsになっていることに依ります。 "module": "commonjs" amdがブラウザ用ということですが、最近の流れはNodeJS用のcommonjsをWebPackの力でブラウザで実行できるようにするのが流れのようです。

WebPackのインストール

これまで通り、まずは手動でインストールします。

$ sudo docker-compose run --rm hellotsagain bash
# yarn global add webpack
yarn global v1.2.1
warning You are using Node "9.0.0" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || ^8.0.0"
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
warning Your current version of Yarn is out of date. The latest version is "1.3.2" while you're on "1.2.1".
info To upgrade, run the following command:
$ curl -o- -L https://yarnpkg.com/install.sh | bash
success Installed "webpack@3.8.1" with binaries:
      - webpack
Done in 9.69s.
# webpack
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory.
Use --help to display the CLI options.

webpack.config.jsを用意する

module.exports = {
  entry: './src/hellotsagain.ts',
  output: {
    filename: './web.js'
  },
  resolve: {
    extensions: ['.ts']
  },
  module: {
    loaders: [
      {
        loader: 'ts-loader'
      }
    ]
  }
}

「./src/hellotsagain.ts」から依存を順に解決していき、必要なものをすべて含んだJavaScriptファイルを「./web.js」として出力するように設定しました。

それからts-loaderというWebPackとTypeScriptを繋ぐモジュールが必要でした。これもインストールしましょう。

# yarn add --dev ts-loader
...

それではWebPackを使ってみます。

# webpack
Hash: 1e07834c467f20db94df
Version: webpack 3.8.1
Time: 1251ms
   Asset     Size  Chunks             Chunk Names
./web.js  2.77 kB       0  [emitted]  main
   [0] ./src/hellotsagain.ts 273 bytes {0} [built]

出力されたweb.jsはwebpackによりかなり長くなりました。

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     function __webpack_require__(moduleId) {
/******/
/******/             // Check if module is in cache
/******/             if(installedModules[moduleId]) {
/******/                     return installedModules[moduleId].exports;
/******/             }
/******/             // Create a new module (and put it into the cache)
/******/             var module = installedModules[moduleId] = {
/******/                     i: moduleId,
/******/                     l: false,
/******/                     exports: {}
/******/             };
/******/
/******/             // Execute the module function
/******/             modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/             // Flag the module as loaded
/******/             module.l = true;
/******/
/******/             // Return the exports of the module
/******/             return module.exports;
/******/     }
/******/
/******/
/******/     // expose the modules object (__webpack_modules__)
/******/     __webpack_require__.m = modules;
/******/
/******/     // expose the module cache
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // define getter function for harmony exports
/******/     __webpack_require__.d = function(exports, name, getter) {
/******/             if(!__webpack_require__.o(exports, name)) {
/******/                     Object.defineProperty(exports, name, {
/******/                             configurable: false,
/******/                             enumerable: true,
/******/                             get: getter
/******/                     });
/******/             }
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/             var getter = module && module.__esModule ?
/******/                     function getDefault() { return module['default']; } :
/******/                     function getModuleExports() { return module; };
/******/             __webpack_require__.d(getter, 'a', getter);
/******/             return getter;
/******/     };
/******/
/******/     // Object.prototype.hasOwnProperty.call
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // __webpack_public_path__
/******/     __webpack_require__.p = "";
/******/
/******/     // Load entry module and return exports
/******/     return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var math;
(function (math) {
    class Calculator {
        sum(a, b) {
            return a + b;
        }
    }
    math.Calculator = Calculator;
})(math = exports.math || (exports.math = {}));


/***/ })
/******/ ]);

もうsrc/hellotsagain.jsは必要ありません。 $ rm src/hellotsagain.js

index.htmlがweb.jsを読み込むようにします。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>もう一度TypeScript</title>
    </head>
    <body>
        <script src="/web.js"></script>
    </body>
</html>

今度はブラウザでアクセスしたときにエラーがでません。WebPackがexportsを解決してくれました。

Dockerfileを更新

もはやtscを自分で使うことはないので、globalインストールから外し、代わりにwebpackを指定します。これまでの yarn add --dev package により、package.jsonが更新されているので、他の依存モジュールについて心配することはありません。

FROM node:9.0.0

WORKDIR /srv

RUN yarn global add webpack
RUN yarn install

Dockerfileはこうなりました。

$ sudo docker-compose build hellotsagain
...
$ sudo docker-compose run --rm hellotsagain webpack
Hash: 1e07834c467f20db94df
Version: webpack 3.8.1
Time: 1240ms
   Asset     Size  Chunks             Chunk Names
./web.js  2.77 kB       0  [emitted]  main
   [0] ./src/hellotsagain.ts 273 bytes {0} [built]

いい感じです。

ファイル構成

今回はこれ以上ファイルを追加しないので、ここでファイル構成を確認しておきます。node_modulesは省いています。

$ tree
.
├── docker-compose.yml
├── Dockerfile
├── index.html
├── package.json
├── src
│   └── hellotsagain.ts
├── tests
│   └── hellotsagain.test.ts
├── tsconfig.json
├── web.js
├── webpack.config.js
└── yarn.lock

DOMのテストをしたい

結論を先に書くと、特になにもする必要がありませんでした。てっきりHeadless ChromeやPhantomJSやその他DOMをNodeJSに持ち込むモジュールを組み合わせないといけないと思っていたのですが、いきなり動きました。5年の間に状況は変わっていたようです。

DOMのテストのために関数を追加

export module math {
    export class Calculator {
        sum(a: number, b: number): number {
            return a + b;
        }
    }
}

export function updateTitle(title: string): void {
    document.title = title;
}

document.addEventListener("DOMContentLoaded", (): void => {
    updateTitle("Hello World");
});

コンパイルした後にブラウザでアクセスすれば、タイトルが変更されることが確認できます。 $ sudo docker-compose run --rm hellotsagain webpack

テストを追加する

import { math, updateTitle } from '../src/hellotsagain';

it('足し算', () => {
    const calculator = new math.Calculator();

    expect(calculator.sum(1, 2)).toBe(3);
});

it('タイトルの更新', () => {
    const title = 'new title';
    updateTitle(title);
    expect(document.getElementsByTagName('title')[0].text).toBe(title);
});
$ sudo docker-compose run --rm hellotsagain npm test

> hellotsagain@0.0.0 test /srv
> jest

 PASS  tests/hellotsagain.test.ts
  ✓ 足し算 (2ms)
  ✓ タイトルの更新 (7ms)

  console.log tests/hellotsagain.test.ts:9
    Node.js (linux; U; rv:v9.0.0) AppleWebKit/537.36 (KHTML, like Gecko)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.699s
Ran all test suites.

クリーンナップ

npm run buildでWebPackを動かす

Jestを npm test で実行するようにできたように、WebPackも実行できるようにします。ただし npm build とすることはできないようです。いくつかのscriptsだけがrunを省略できるようです。

package.jsonのscriptsにbuildを足します。

{
  "name": "hellotsagain",
  "version": "0.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@types/jest": "^21.1.6",
    "jest": "^21.2.1",
    "ts-jest": "^21.2.1",
    "ts-loader": "^3.1.1",
    "typescript": "^2.6.1"
  },
  "scripts": {
    "test": "jest",
    "build": "webpack"
  },
  "jest": {
    "transform": {
      "\\.ts$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
    },
    "testRegex": "\\.test\\.ts$",
    "moduleFileExtensions": [
      "js",
      "ts"
    ]
  }
}

全体はこうなりました。

$ sudo docker-compose run --rm hellotsagain npm run build

> hellotsagain@0.0.0 build /srv
> webpack

Hash: 1abb300381b4b774823d
Version: webpack 3.8.1
Time: 1199ms
   Asset     Size  Chunks             Chunk Names
./web.js  2.95 kB       0  [emitted]  main
   [0] ./src/hellotsagain.ts 458 bytes {0} [built]

いい感じです。jestの時に確認し忘れていましたが、npmから実行するときはglobalにインストールしなくてもパスが通ります。インストールしたモジュールのbinにパスが自動的に通されます。よってwebpackをglobalにインストールしなくてもよくなりました。

まずはwebpackをglobalなしでインストールします。

$ sudo docker-compose run --rm hellotsagain yarn add --dev webpack
...

これをこのタイミングで実行したのは、次にWebPackをグローバルにインストールしなくなったあとのコンテナのビルドでWebPackがインストールされるようにするためです。

FROM node:9.0.0

WORKDIR /srv

RUN yarn install

Dockerfileからglobalへのインストールを完全に無くすことができました。

$ sudo docker-compose build hellotsagain
...
$ sudo docker-compose run --rm hellotsagain webpack
ERROR: Cannot start service hellotsagain: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"webpack\\\": executable file not found in $PATH\"\n"

WebPackがglobalから消えています。

しかし npm run build はできます。

$ sudo docker-compose run --rm hellotsagain npm run build

> hellotsagain@0.0.0 build /srv
> webpack

Hash: 1abb300381b4b774823d
Version: webpack 3.8.1
Time: 947ms
   Asset     Size  Chunks             Chunk Names
./web.js  2.95 kB       0  [emitted]  main
   [0] ./src/hellotsagain.ts 458 bytes {0} [built]

docker-composeのcommandを変える

sudo docker-compose run --rm hellotsagain npm run buildsudo docker-compose run --rm hellotsagain npm test とこれまでしてきましたが、何もしていせずにrunすると「Hello World」と出力されるだけです。

$ sudo docker-compose run --rm hellotsagain
Hello World

これはこのブログポストの最初に書いたdocker-compose.ymlでそう指定してあるからです。おそらく最も頻繁に実行したいのはテストなので、デフォルトでテストを実行するようにします。

docker-compose.ymlのcommandを書き換えます。

hellotsagain:
  build: .
  command: npm test
  volumes:
    - .:/srv

ファイル全体ではこうなりました。

$ sudo docker-compose run --rm hellotsagain

> hellotsagain@0.0.0 test /srv
> jest

 PASS  tests/hellotsagain.test.ts
  ✓ 足し算 (3ms)
  ✓ タイトルの更新 (3ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.702s
Ran all test suites.

できました。

終わり

以上、TypeScriptへの再挑戦の記録でした。しかし、ここまでではまだ環境構築しかできていません。バージョンが2.6.1になったTypeScriptは5年前よりもとても良くなっているはずなので、新しい機能や文法も覚えないといけません。

なんにせよ、TypeScriptを書く環境は整えられました。

コメント

2015 - 2017 (c) 成瀬基樹