フロントエンド環境（TypeScript+PUG+SASS+Jest）を作ってみる

# 概要

フロントエンドを勉強しよーって時にUIライブラリを使わずに自分で色々作ってみることがあったり無かったりすると思うのですが、そういう時に使う環境をちゃちゃっと用意しようと思います。新人さんの勉強とかにも使えるのかも？
細かい説明は割愛して動かせる環境を用意することを目的とします。細かいところ説明してって言われても理解してない部分が多いのです...精進します(´・ω・｀)

# package.jsonとちょっとしたコード

[frontend-dev-env: TypeScript+PUG+SASS+Jest](https://github.com/yoshida-san/frontend-dev-env)

# 必要な時間

詰まらなければ1時間程度で終わると思います。

# 環境

TypeScript+PUG+SASSで書けるようにします。単体テストはJestを使います。結合はTestCafeを使えば良いと思います。IDEはVisual Studio Code。

# 開発環境構築

VSCode及びNode.jsのインストールは割愛。`npm init -y`しておいてください。

## TypeScript

### 本体のインストール

```sh
$ npm install typescript
$ npm install --save-dev tslint
```

## TSLint

### 設定

tslintのinitを行います。

```sh
$ ./node_modules/.bin/tslint --init
```

作られたtslint.jsonをちょこちょこっと修正。

```json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "interface-name": false,
        "no-empty-interface": true,
        "object-literal-sort-keys": false,
        "member-access": true,
        "typedef": true,
        "triple-equals": true,
        "variable-name": false,
        "radix": false,
        "align": false,
        "max-line-length": false,
        "member-ordering": false,
        "curly": false
    },
    "rulesDirectory": []
}
```

### 動作確認

`./src/ts/sample.ts`を作成し、以下のコードを記述。

```js
var num: number = 1;
```

コードを書いたら以下のコマンドでTypeScriptのコードをチェックします。

```sh
$ ./node_modules/.bin/tslint './src/**/*.ts'
```

先の例だと以下のようなメッセージが表示されます。

```txt
ERROR: src/ts/sample.ts:1:1 - Forbidden 'var' keyword, use 'let' or 'const' instead
ERROR: src/ts/sample.ts:1:12 - file should end with a newline
```

varを使わないでletかconstを使って、最後に1行入れてね。ってことですね。直してみましょう（最終行の空行は省略します）。

```js
const num: number = 1;
```

再度チェックするとエラーは無くなってますね！例えば`1`ではなく`"文字列"`を入れた場合もエラーに引っかかって欲しいですが、VSCodeの方で警告が出るのでそっちで直していきましょう。

### npm scripts登録

package.jsonのscriptsに以下を追加しておきます。

```json
"scripts": {
  "tslint": "./node_modules/.bin/tslint './src/**/*.ts --silent'"
}
```

--silentオプションを付けないとExit status 2（または1）が表示されてしまうので、隠してます。`npm run tslint`でsrc配下の.tsファイルのチェックが可能です。

## tsc

### 設定

```sh
$ ./node_modules/.bin/tsc --init
```

tsconfig.jsonはトップレベルに以下を追加。

```json
{
  "include": [
    "./src/**/*.ts"
  ],
  :
}
```

後は`"outDir"`のコメントアウトを削除して値を`"./dist"`に、`"lib"`のコメントアウトを削除して値を`["es6"]`しておきます。

### 動作確認

```sh
$ ./node_modules/.bin/tsc src/ts/sample.ts --outDir dist/js
```

先程のtsファイルを`./dist/js/`にJavaScriptに変換して出力します。出力したファイルの中身は以下のようになっているかと思います。

```js
var num = 1;
```

constがvarになって、型定義が無くなってますね！もうちょっとわかりやすくするためにsample.tsを以下のように修正してJavaScriptに変換してみましょう。

```js
export default class User {

    private _firstName: string;
    private _lastName: string;
    private _birthday: string;
    private _sex: number;
    private _activate: boolean;

    public constructor(firstName: string = "", lastName: string = "", birthday: string = "2000/01/01", sex: number = 0, activate: boolean = false) {
        this._firstName = firstName;
        this._lastName = lastName;
        this._birthday = birthday;
        this._sex = sex;
        this._activate = activate;
    }

    public setFirstName = (arg: string): void => { this._firstName = arg; };
    public setLastName = (arg: string): void => { this._lastName = arg; };
    public setBirthday = (arg: string): void => { this._birthday = arg; };
    public setSex = (arg: number): void => { this._sex = arg; };
    public setActivate = (arg: boolean): void => { this._activate = arg; };

    private splitBirthday = (birthday: string): number[] => birthday.split("/").length > 1 ? birthday.split("/").map((s) => parseInt(s)) : birthday.split("-").map((s) => parseInt(s));

    private verifyBirthday = (birthday: string): boolean => {
        const dateFormat: RegExp = /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/;
        if (!birthday.match(dateFormat)) return false;
        const splitedDate: number[] = this.splitBirthday(this._birthday);
        if (splitedDate.length !== 3) return false;
        const ListOfDays: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        if (splitedDate[1] === 1 || splitedDate[1] > 2) {
            if (splitedDate[2] > ListOfDays[splitedDate[1] - 1]) return false;
        }
        if (splitedDate[1] === 2) {
            const leapYearFlag = (!(splitedDate[0] % 4) && splitedDate[0] % 100) || !(splitedDate[0] % 400) ? true : false;
            if ((leapYearFlag === false) && (splitedDate[2] >= 29)) return false;
            if ((leapYearFlag === true) && (splitedDate[2] > 29)) return false;
        }
        return true;
    }

    public getFullName = (): string => `${this._firstName} ${this._lastName}`;
    public getAge = (): number => {
        if (!this.verifyBirthday(this._birthday)) return -1;
        const splitedBirthday: number[] = this.splitBirthday(this._birthday);
        const d = new Date();
        const now = parseInt(d.getFullYear().toString() + ("0" + (d.getMonth() + 1).toString()).slice(-2) + ("0" + d.getDate().toString()).slice(-2));
        const birthday = parseInt(("000" + splitedBirthday[0].toString()).slice(-4) + ("0" + (splitedBirthday[1].toString())).slice(-2) + ("0" + (splitedBirthday[2].toString())).slice(-2));
        return Math.floor((now - birthday) / 10000);
    }

    public getSex = (): string => {
        switch (this._sex) {
            case 0:
                return "男性";
            case 1:
                return "女性";
            default:
                return "";
        }
    }

    public isActivate = (): boolean => this._activate;

    public toJson = (): object => {
        return {
            name: this.getFullName(),
            birthday: this._birthday,
            age: this.getAge(),
            sex: this.getSex(),
            activate: this.isActivate(),
        };
    }

}
```

ちょっと長いですが、TypeScriptとJavaScriptのファイルで違いがよくわかると思います。

### npm scripts登録

変換も簡単に使えるようにしておきましょう。package.jsonのscriptsに以下を追加しておきます。

```json
"scripts": {
  "tsc": "./node_modules/.bin/tsc --rootDir src"
}
```

`npm run tsc`でTypeScriptからJavaScriptに変換が可能です。出力先はoutDirで指定したdistディレクトリになります。

## PUG

ejsでも良いのですが、SASSっぽいしSASSはPUGっぽいので合わせようと思います。Expressとかでも使えるらしいので無駄にはならない...かな。

### 設定

```sh
$ npm install -D pug-cli
```

特にconfigは不要です。

### 動作確認

src直下にindex.pugを作成して以下を記述します。

```pug
doctype html
html(lang="ja")
  head
    meta(charset="utf-8")
    title PUG Sample
  body
    h1 h1 title!
    main.main-class#main-id
      h2 h2 title :-)
```

./src/ts直下にindex.pugを作成して以下を記述します。

```pug
doctype html
html(lang="ja")
  head
    meta(charset="utf-8")
    title tsディレクトリ
  body
    p tsディレクトリ内のindex.pug
```

ファイルの作成が終わったら以下のコマンドを実行しましょう。

```sh
$ ./node_modules/.bin/pug ./src --out ./dist
```

dist直下とdist/ts直下にそれぞれindex.htmlができたかと思います。HTMLが見づらい！という場合は`--pretty`オプションを付与して実行すると見易い形で出力されます。PUGは慣れないと少し書きづらいですが、慣れてくると非常に書きやすいので頑張って慣れましょう（気合）。

### npm scripts登録

例のごとくnpmで実行できるようにしておきましょう。

```json
"scripts": {
  "pug": "./node_modules/.bin/pug ./src --out ./dist"
}
```

## SASS

### 設定

毎度の通り以下のコマンド。

```sh
$ npm install -D node-sass
```

### 動作確認

src直下にstyleディレクトリを作成し、その中にsample.sassを作成し以下を記述。

```sass
body
  background-color: lightgray
  margin: 0 auto
  h1
    font-weight: bold
  .main-class
    h2
      color: red
```

src直下のindex.pugのtitleの上に以下の一行を追加し（インデックスは合わせてください）、htmlを作成しておきます。

```pug
link(rel="stylesheet" type="text/css" href="/style/sample.css")
```

追加してhtmlを出力したらsassをcssにして出力しましょう。

```sh
$ ./node_modules/.bin/node-sass ./src -o ./dist -r
```

dist配下にstyleディレクトリができてその中にsample.cssができていればOKです！PHPビルトインサーバとかでdist直下のindex.htmlを見るとCSSが適用されているのがわかります（ブラウザでファイルを開いても確認できます）。

### npm scripts登録

npmの登録は圧縮された状態で出力するようにしておきましょう。

```json
"scripts": {
  "sass": "./node_modules/.bin/node-sass ./src -o ./dist --output-style compressed -r"
}
```

## Jest

### 設定

```sh
$ npm install -D jest @types/jest ts-jest
```

package.jsonに以下を追加。

```json
"jest": {
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
  "testMatch": [
    "**/test/**/*.ts?(x)"
  ]
}
```

### 動作確認

srcと同階層にtestディレクトリを作成し、その中にsample.test.tsを作成して以下を記述。

```ts
import User from "../src/ts/sample";

describe("Userクラスのテスト", () => {
    const u = new User("Hoge", "Piyo", "1984/4/4", 0, true);
    test("名前取得", () => {
        expect(u.getFullName()).toBe("Hoge Piyo");
    });
    test("年齢算出", () => {
        expect(u.getAge()).toBe(35);
    });
    test("性別取得", () => {
        expect(u.getSex()).toBe("男性");
    });
});
```

以下のコマンドでテスト実行。

```sh
$ ./node_modules/.bin/jest
```

こんな感じでテスト結果が表示されたらOKです！

```sh
PASS  test/sample.test.ts
 Userクラスのテスト
   ✓ 名前取得 (3ms)
   ✓ 年齢算出 (1ms)
   ✓ 性別取得

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

### npm scripts登録

いつも通りですね。カバレッジを出力するようにしておきます。

```json
"scripts": {
  "jest": "./node_modules/.bin/jest --coverage"
}


```

## 環境構築で作成したnpm scriptsのまとめ

### npm run tslint

TypeScriptの静的コードチェックを行います。

### npm run tsc

TypeScriptをJavaScriptに変換します。

### npm run pug

.pugを.htmlに変換します。

### npm run sass

.sassを.cssに変換します。

### npm run jest

TypeScriptのテスト（testディレクトリ配下のテストコード）を実行します。

# ディレクトリ構成の整理

開発環境がある程度整ったので、ディレクトリ構成を整理しておこうと思います。

```txt
root
 ├ coverage: テストカバレッジ出力用ディレクトリ
 ├ dist: ビルドコード出力用ディレクトリ
 ├ src: ビルド前のソースコード用ディレクトリ
 │  ├ style: sass書く用のディレクトリ
 │  └ script: ts書く用のディレクトリ
 ├ test: テストコード用ディレクトリ
 ├ node_modules: Node.jsパッケージ用ディレクトリ
 ├ package-lock.json: パッケージの依存関係情報とか（たしか
 ├ package.json: 必要なパッケージやnpm scripts等々
 ├ tsconfig.json: TypeScript設定
 └ tslint.json: TSLint設定
```

ディレクトリ構成はお好きに変えて問題ありませんが、この記事の内容を加味するとこんな感じになります。

# 最後に

とりあえず勢いだけで構築しました。sample.tsの名前が中身と合ってないなぁと思いつつ。色々と無知を晒していると思いますが、「ここはこうやった方がいい」とか「これはアンチパターンやぞ」とかありましたらご教授頂けると嬉しいです。
TestCafeを使っての結合テストやGrunt（Gulp）とかWebPack、git云々もやろうと思いましたが力尽き（飽き）ました...。