Nuxt.jsでTypeScriptとJSXとJest

世界中の"むずかしい"を簡単にする[株式会社diffeasy](https://diffeasy.com/)で[日本一魅力的なプログラマー集団を目指すCTO西](https://twitter.com/_takeshi_24)です。

この記事は[Geeks Who Drink in Fukuoka -Front End Edition 2-](https://nulab.connpass.com/event/120998/)でのLTの内容を記事にしたものです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/3003/3b5c1a76-4d29-6710-eee9-c7128157014c.png)


当日発表スライドはこちら。
[Nuxt.jsで TypeScriptとJSXとJest](https://speakerdeck.com/takec24/nuxt-dot-jsde-typescripttojsxtojest)

ソースは[GitHub](https://github.com/tkc24/nuxt-ts-jsx)にあげています。

# Nuxt.jsとTypeScript
![image.png](https://qiita-image-store.s3.amazonaws.com/0/3003/e7b18d4d-dc3d-647b-0325-9552b0417e6f.png)

[Nuxt.js](https://ja.nuxtjs.org/)と[TypeScript](https://www.typescriptlang.org/)については、こちらでは説明を省略します。

フロントエンドはTypeScriptで書きたい！という人が最近増えてきたように思います。
Nuxt.jsはTypeScriptをサポートしていなかったのですが、[Ver.2.4.0](https://ja.nuxtjs.org/guide/release-notes/)からTypeScriptがサポートされるようになりました。

# Nuxt.jsとJSX
JSX(「JavaScript eXtension」の略)はReactで利用されるJavaScriptの記法です。
JSX=Reactというイメージですが、Vue.js(Nuxt.js)でも利用することができます。

ちなみに、JSXでGoogle検索しようとすると、候補キーワードとして「気持ち悪い」が出てきます笑
<img width="925" alt="JSX気持ち悪い" src="https://qiita-image-store.s3.amazonaws.com/0/3003/90dca87f-db5a-ac14-fd61-58c4e397d076.png">
私自身も初めてJSXに出会った時の正直な感想は「キモっ！！」でした。
当時は「ロジックとデザインの分離をいかに進めるか？」ということを考えてシステム設計されることが多かったと思います。
弊社もエンジニアのスキル、体制を考慮して、ロジックとデザインは基本的に分離する設計を進めていました。

しかし、フロントエンド開発において構造・見た目・振る舞いをセットにして考える「コンポーネント志向」が増えてきました。その流れの中で、最近では私も「JSXありなんじゃないか？」と思うようになってきました。


# Nuxt.jsとTypeScriptとJSXでアプリケーション開発
## create-nuxt-appでNuxt.jsアプリケーション構築
Nuxt.jsのアプリケーションは[create-nuxt-app](https://ja.nuxtjs.org/guide/installation/)を利用すると、コマンド１つで簡単に作成できます。まずはこれでNuxt.jsアプリケーションの雛形を作ります。
ただし、2019年3月時点で、TypeScriptには対応していないので、後からTypeScript対応していきます。

以下のコマンドを実行します。```<my-project>```にはプロジェクト名を入れてください。

```terminal
$ yarn create nuxt-app <my-project>
```

実行すると、server framework、UI framework、test frameworkなど利用するフレームワークを聞かれますので、それぞれ選択してください。

今回はJestでテストするので、test frameworkはJestを選択します。
その他はお好みで。

## nuxt-tsでTypeScript対応
nuxtを削除して、nuxt-tsを利用することで、TypeScript対応していきますので、一旦、yarn.lockとnode_modulesを削除します。

```terminal
$ rm ./yarn.lock
$ rm -rf ./node_modules
```

nuxt-tsと[vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)をインストールします。

```terminal
$ yarn add nuxt-ts
$ yarn add vue-property-decorator
```

package.jsonのScriptをnuxtからnuxt-tsに書き換えます。

```json:/package.json
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "start": "nuxt-ts start",
    "generate": "nuxt-ts generate",
    "test": "jest"
  },
```

tsconfig.jsonにTypeScriptを利用するための設定を追加します。
(この後の手順で再度tsconfig.json編集します。)

```/json:tsconfig.json
{
  "extends": "@nuxt/typescript",
  "compilerOptions": {
    "baseUrl": ".",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "allowJs": true,
    "types": [
      "@types/node",
      "@nuxt/vue-app"
    ]
  }
}
```

## Nuxt.jsでJSXを利用する
tsconfig.jsonにJSXを利用するための設定を追加します。

```/json:tsconfig.json
{
  "extends": "@nuxt/typescript",
  "compilerOptions": {
    "baseUrl": ".",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "allowJs": true,
    "module": "commonjs",
    "jsxFactory": "h",
    "types": [
      "@types/node",
      "@nuxt/vue-app"
    ]
  }
}
```

コンポーネントを作成します。
/components配下に、*.tsxの拡張子でファイルを作成します。
ここでは、HelloWorld.tsxとします。

```tsx:/components/HelloWorld.tsx
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class HelloWorld extends Vue {
  @Prop({ default: 'TypeScript!' }) readonly name!: string
  message: string = 'Hello, '
  render(h: Vue.CreateElement): Vue.VNode {
    return (
      <div>
        <p>{this.message} {this.name}</p>
      </div>
    )
  }
}
```
通常のVueでは、*.vueファイル内にtemplateとscriptタグでテンプレートとロジックを書きますが、JSXを利用する場合は、render関数の中でJSXをレンダリングします。

pagesからHelloWorld.tsxを読み込みます。

```vue:/pages/index.vue
<template>
・・・
      <HelloWorld name="Takeshi"/>
・・・
</template>
<script>
import HelloWorld from '~/components/HelloWorld.tsx'
export default {
  components: {
    ・・・,
    HelloWorld
  }
}
</script>
```

devサーバーを起動して、ブラウザで http://localhost:3000 を開きます。

```terminal
$ yarn run dev
```

<img width="622" alt="NuxtJSX.png" src="https://qiita-image-store.s3.amazonaws.com/0/3003/42ed3781-89e6-1286-1bc4-3c2dc19e834b.png">

起動して、コンポーネントが表示されました。

# Jestでテスト
続けてコンポーネントのテストを書きます。

TypeScriptのテストを書くために、ts-jestをインストールします。

```terminal
$ yarn add -D ts-jest
```

テストコードはとりあえず簡単に以下の通り。

```js:/test/HelloWorld.spec.js
import { mount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.tsx'

describe('HelloWorld', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(HelloWorld)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})
```

JestでJSXのテストを実行すると、以下のエラーが発生します。

```terminal
    Jest encountered an unexpected token
・・・
    Details:

    SyntaxError: /Users/takeshi/projects/nuxt24/nuxt_ts/components/HelloWorld.tsx: Unexpected token (16:16)

      14 |     }
      15 |     render(h) {
    > 16 |         return (<div>
         |                 ^
      17 |         <p>{this.message} {this.name}</p>
      18 |       </div>);
      19 |     }
```

JestでJSXのテストを実行するためには、tsconfig.jsonに```"jsx": "react"```の設定が必要なのですが、これを追加すると、JSXで書いたコンポーネントが画面に表示されなくなってしまいました。

そこで、テスト用のtsconfig.jsonを新たに作成して回避します。

```json:/tsconfig.test.json
{
  "extends": "@nuxt/typescript",
  "compilerOptions": {
    "baseUrl": ".",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "allowJs": true,
    "jsx": "react",
    "module": "commonjs",
    "jsxFactory": "h",
    "types": [
      "@types/node",
      "@nuxt/vue-app"
    ]
  }
}
```

jest.config.jsからこちらのテスト用のtsconfigを読み込みます。
globals -> ts-jest -> tsConfigFileの設定箇所です。

```js:/jest.config.js
module.exports = {
  globals: {
    'ts-jest': {
      useBabelrc: true,
      tsConfigFile: 'tsconfig.test.json'
    }
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['js', 'vue', 'json', 'tsx'],
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
    "^.+\\.tsx?$": "ts-jest"
  },
  "collectCoverage": true,
  "collectCoverageFrom": [
      "<rootDir>/components/**/*.vue",
      "<rootDir>/pages/**/*.vue"
  ]
}
```

テストを実行します。

```
$ yarn test
```

テスト成功しました！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/3003/0dfe3b60-f1d8-6bb0-6191-9168cdd18b9b.png)

# 感想
Nuxt.js Ver.2.4.0からTypeScirptサポートされるようになったとはいえ、色々なライブラリと連携させようとするとなかなかスムーズにはいきませんでした。

あと個人的な感想ですが、JSXだとrenderのなかにあまりダラダラとHTMLを書きたくないので、自然とコンポーネントの単位が小さくなりそうな気がします。