Elm+TypescriptにVSCodeのLinter設定してGitHubActionsでgithub.ioに公開したメモ

## 概要
プログラミングElmを買った。
開発を試すための準備を行ったのでメモを残す。

[ソースコード](https://github.com/hibohiboo/programming-elm/tree/9771fad7024428c90e4216c3d231140ff65c494b/)


## やること
* webpack5を試す
  * 試した結果、webpack4のときと違いなし
* typescriptからElmを呼び出す
  * 型付きの世界を知ってしまうともうJSには戻りたくない
* VSCodeで開発しやすくする
  * linterは必須。細かいインデント直すのとか嫌
* github.ioに自動デプロイ
  * 成果は見れるようにしたい

## 環境
* Windows 10
* Node 14.16.1
* npm グローバルインストール
  * elm-analyse@0.16.5
  * elm-format@0.8.5
  * elm-test@0.19.1-revision6
  * @elm-tooling/elm-language-server@2.0.3

以下、npm installでpackage.jsonのパッケージをインストールしていることを前提で記す。
<details><summary>package.json</summary><div>

```json:package.json
{
  "name": "eiseiteien",
  "version": "0.0.0",
  "description": "elmのサンプル",
  "main": "index.js",
  "scripts": {
    "dev": "webpack",
    "prod": "webpack",
    "serve": "webpack  serve"
  },
  "repository": {
    "type": "git",
    "url": "git@ssh.dev.azure.com:v3/hibohiboo/%E8%A1%9B%E6%98%9F%E5%BA%AD%E5%9C%92/aws-elm"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@elm-tooling/elm-language-server": "^2.1.0",
    "@typescript-eslint/eslint-plugin": "^4.22.1",
    "@typescript-eslint/parser": "^4.22.1",
    "css-loader": "^5.2.4",
    "elm": "^0.19.1-5",
    "elm-format": "^0.8.5",
    "elm-hot-webpack-loader": "^1.1.8",
    "elm-webpack-loader": "^8.0.0",
    "eslint": "^7.25.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-import-resolver-webpack": "^0.13.0",
    "eslint-plugin-prettier": "^3.4.0",
    "html-webpack-plugin": "^5.3.1",
    "prettier": "^2.2.1",
    "style-loader": "^2.0.0",
    "ts-loader": "^9.1.2",
    "typescript": "^4.2.4",
    "webpack": "^5.36.2",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^4.0.0-beta.3"
  }
}
```

</div></details>

## VSCodeへLinterの導入

* ソースコード保存時にフォーマットされるようにする
  * editorconfig
  * eslint
  * prettier
  * elmtooling
* srcフォルダに`~`のエイリアスを切っている。eslint,tsconfig,webpackの3か所に設定が必要[*参考][*2]

<details><summary>設定ファイル</summary><div>

```json:.vscode/extensions.json
{
  "recommendations": [
    "editorconfig.editorconfig",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "elmtooling.elm-ls-vscode",
  ],
  "unwantedRecommendations": []
}
```

```json:.vscode/settings.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
  },
  "editor.formatOnSave": true,
  "editor.tabCompletion": "on",
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  // デフォルトの設定はオフにしておく
  "typescript.format.enable": false,
  "javascript.format.enable": false,
  "liveServer.settings.port": 5502,
  "[scss]": {
    "editor.formatOnSave": false,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
  },
  "css.validate": false,
  "scss.validate": false,
}
```

```config:.editorconfig
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
````

```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": ["./src/*"]
    },
    "outDir": "./dist/",
    "sourceMap": true,
    "target": "es6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "noImplicitAny": true,
  },
  "exclude": ["node_modules", "__tests__", "cdk"],
  "include": ["next-env.d.ts", "**/*.ts"]
}

```
```js:.eslintrc.js
const path = require('path')

module.exports = {
  ignorePatterns: ['.eslintrc.js', '.babelrc.js', 'webpack.config.js', 'public'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:prettier/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint'],
  parser: '@typescript-eslint/parser',
  env: {
    browser: true,
    node: true,
    es6: true,
    jest: true,
  },
  parserOptions: {
    sourceType: 'module',
  },
  rules: {
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    'commma-dangle': 'off',
  },
  settings: {
    'import/resolver': {
      webpack: { config: path.join(__dirname, '/webpack.config.js') },
    },
  },
}

```


```js:.prettierrc.js
module.exports = {
  semi: false,
  arrowParens: 'always',
  singleQuote: true,
  trailingComma: 'all',
}
```


```json:elm-tooling.json
{
  "entrypoints": [
    "./src/elm-files/Main.elm"
  ]
}
```
</div></details>

## フォルダ構造

```
- public
  - template.html
  - main.css
- src
  - elm-files
    - Main.elm
    - Main.elm.d.ts
  - entry
    - index.ts
```

## Webpack

* ts,elmを1つのjsにバンドル
* elmは`npm run dev`,`npm run serve`の時はデバッグを有効にする
* css,htmlはpublicフォルダ内にあるものを利用

<details><summary>設定ファイル</summary><div>

```js:webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const MODE = ['dev', 'serve'].includes(process.env.npm_lifecycle_event)
  ? 'development'
  : 'production'

const elmRuleBase = {
  test: /\.elm$/,
  exclude: [/elm-stuff/, /node_modules/],
  use: [],
}

const elmRule =
  MODE === 'development'
    ? {
        ...elmRuleBase,
        use: [
          { loader: 'elm-hot-webpack-loader' },
          {
            loader: 'elm-webpack-loader',
            options: {
              debug: true,
            },
          },
        ],
      }
    : {
        ...elmRuleBase,
        use: [
          {
            loader: 'elm-webpack-loader',
            options: {
              optimize: true,
            },
          },
        ],
      }

module.exports = {
  mode: MODE,
  entry: './src/entry/index.ts',
  devtool: 'source-map', // 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: { url: false },
          },
        ],
      },
      elmRule,
    ],
  },
  resolve: {
    alias: {
      '~': path.resolve(__dirname, 'src'),
    },
    extensions: ['.ts', '.js'], //  .jsがないと、Can't resolve 'xxxx' が発生する
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack Boilerplate',
      template: path.resolve(__dirname, './public/template.html'),
      filename: 'index.html',
    }),
  ],
}
```

</div></details>

```html:public/template.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>
```


## Elm Hello
* フォーマッタが効くかどうか、 ` main =text "Html hello!"` を一行にしてみるなどして試す
  * 効いていない場合、vscodeを再起動してみると直ることが多い
  * それでもダメなら、一度`npm run serve`するなどしてelmをトランスパイル後、もう一度VSCodeを再起動

```elm:src/elm-files/Main.elm
module Main exposing (main)

import Html exposing (Html, text)


main : Html msg
main =
    text "Html hello!"

```

## Typescript EntryPoint
* `;`を入れて保存するなど、eslintが効いていることを確認
* Elmとhtmlのidを紐づける作業
* Elmの型は`.d.ts`の型ファイルを書いてTypescriptに認識させる [*参考][*1]

```ts:src/entry/index.ts
import { Elm } from '~/elm-files/Main.elm'
import '~/../public/main.css'
Elm.Main.init({
  node: window.document.getElementById('root'),
})
```

```ts:src/elm-files/Main.elm.d.ts
export namespace Elm {
  namespace Main {
    interface Args {
      node: HTMLElement
      flags?: Flags
    }

    interface Flags {
      initialValue: string
    }

    function init(args: Args)
  }
}
```

## 動作確認
### ホットリロードの確認
  * `npm run serve`で起動
  * http://localhost:8080/ に アクセス
  * Html hello!とテキストが出ていることを確認
  * テキストを書き換えて、ホットリロードされることを確認
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/99ab18f4-06da-198b-1b51-65a4ebde28d8.png)

### デバッグログの確認
* ログ出力をソースにいれる

```elm:src/elm-files/Main.elm
module Main exposing (main)

import Html exposing (Html, text)


main : Html msg
main =
    let
        _ =
            Debug.log "log" "test"
    in
    text "Html hello!"
```

* 開発者ツールのコンソールでログを出力できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/11d4b8c3-60a8-967d-f80b-b37866e36f18.png)

* `npm run dev`でトランスパイルできることを確認
* `npm run prod` でトランスパイルが失敗することを確認。Elmのエラーメッセージはとても丁寧。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/38b58eb8-2250-16f6-78c4-5fb3651babd5.png)

## Github.ioへのデプロイ
Github Actionsを利用する。
actions用のyamlファイルを作成。
deployブランチを切ってpush

<details><summary>Actionファイル</summary><div>

```yml:.github/workflows/gh-pages.yml
name: github pages

on:
  push:
    branches:
      - deploy  # Set a branch name to trigger deployment

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '14'

      - name: modules Setup
        run: npm i
        working-directory: .

      - name: trancepile
        run: npm run dev
        working-directory: .


      - name: Move files
        run: |
          mv ./dist ./docs

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./docs
```
</div></details>

* gh-pagesブランチが作成されていることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/92744bd1-89f4-7511-1ecb-ff889ae7e5e0.png)

* SettingsからGitHub Pagesの有効化を行う
  * ブランチはgh-pages,フォルダは(/root)を選択
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/8819d848-fe11-5ff1-5399-5abafae205d3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/5e7e6ece-04f5-c287-9e3b-e19bd269fc6e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/96460539-40fe-a8b2-37f4-646c70149d6f.png)

* 表示されるURLにアクセスし、ページが表示されることを確認する。
  * https://hibohiboo.github.io/programming-elm/

## 参考
[『プログラミングElm』サポートサイト](https://book.mynavi.jp/supportsite/detail/9784839970048.html)
[React: import時のaliasを設定するときはWebpack、TypeScript、ESLintの3つを対応しなければならない件][*2]
[TypeScript から Elm を import する時にエラーを出さない方法 (Parcel)][*1]
[GitHub Actionsを使ってGithub PagesにOpen APIのドキュメントを公開したメモ][*3]

[*1]:https://qiita.com/mizkichan/items/da0e0208675e7ae3092f
[*2]:https://qiita.com/Statham/items/8a1161c7816e360590f3
[*3]:https://qiita.com/hibohiboo/items/a1568d9ecaf9ebb25e11