webpack4 で sass/tsファイル群を変換＆バンドルしてhtmlと一緒に別フォルダに配置する

# はじめに

webpackの名前位しか知らない状態から、以下やりたかったことができる状態にできたため、webpackの設定方法等散らばっていた情報をざっくり本記事にまとめる。

- 自身で作成したscss/tsファイルをそれぞれ1ファイル(css/js)にまとめて（バンドルして）、別のフォルダに配置
  - tsファイル：自動フォーマット → jsに変換かつ es5にトランスパイル → bundle.js生成/配置
  - scss/cssファイル：scssをcssに変換 → style.css生成/配置
  - htmlファイル：htmlも一緒に配置

[できたもの](https://qiita.com/SYM_simu/items/831c13606df8bc1d7baf#%E3%81%A7%E3%81%8D%E3%81%9F%E3%82%82%E3%81%AE) をgithubに置いているため必要あればご参照ください

※諸事情により今回はフレームワークやライブラリを使用しないため対象外

# webpack 導入

npm使用

```
npm init -y
npm i -D webpack webpack-cli
```

# webpack 実行（小規模サンプル）

例：app.js と user.js をまとめて dist 下に bundle.js として配置


- 実行コマンド ※面倒なら PATH を通す

```
.\node_modules\.bin\webpack
```

- webpack.config.js 設定例

```js
// output.pathに絶対パスを指定する必要
const path = require("path");

module.exports = {
  // モードの設定
  mode: "development",
  // エントリーポイントの設定
  entry: "./src/js/app.js",
  // 出力の設定
  output: {
    // 出力ファイル名
    filename: "bundle.js",
    // 出力先パス（絶対パス要）
    path: path.join(__dirname, "dist/js"),
  },
};
```

- フォルダ構成

```
.
├─dist
│  │  index.html
│  │
│  └─js
│        bundle.js
│
└─src
    └─js
        │  app.js
        │
        └─model
               user.js
```

- app.js

```js
import getUser from "./model/user";

function component() {
  const element = document.createElement("div");
  const user = getUser();
  element.innerHTML = ["Hello", "webpack", user].join(" ");
  return element;
}
document.body.appendChild(component());
```

- user.js

```js
export default function getUser() {
  return "user";
}
```

# webpack.config.js　

webpackの設定ファイル。自作する必要がある

```webpack.config.js
module.exports = {
  // 中身の主な設定項目は以下参照
};
```
設定項目と指定の仕方を簡単に以下にまとめる。

## mode モード指定

```js
mode: "development"; // "production" / "none"
```

## entry エントリポイント指定

バンドル対象のメインとするファイル

・単一指定

```js
entry: "./src/index.js",

// ↓ と解釈される
// entry : {
//   main: "./src/index.js"
// }
```

・複数指定も可能

```js
const { join } = require("path")

entry: {
  app1: join(__dirname, "src/app1/index.js"),
  app2: join(__dirname, "src/app2/index.js"),
  app3: join(__dirname, "src/app3/index.js"),
}
```

## devtool ソースマップ設定

ソースマップの追加方法等を指定

```js
devtool: "source-map",
```

[webpack devtool ソースマップ まとめ](https://golang.hateblo.jp/entry/webpack-devtool-source-map)

## output バンドルファイル設定
設定項目

- filename : 出力ファイル名
- path : 出力ディレクトリ（絶対パス）

```js
entry: "./src/index.js",
output: {
  filename: 'bundle.js',
  path: path.join(__dirname, 'dist'),
},
```

```js
entry: "./src/index.js",
output: {
  filename: '[name].js',
  path: path.join(__dirname, 'dist'),
},
// → main.jsが出力される
```

```js
entry: "./src/index.js",
output: {
  filename: '[name].js',
  path: path.join(__dirname, 'dist'),
},
// → app.jsが出力される
```

## externals バンドル対象から除外

```js
externals: {
  "react": "React",
  "react-dom": "ReactDOM"
},
```

※必要なら、ライブラリをバンドル対象から外して html に直指定

## module.rules モジュール適用

設定項目

- test : 適用対象にするファイル
- use : (インストールした)モジュール適用。※下から上の順番に適用される。

以下、モジュールごとの設定例をまとめる。



### JavaScript

#### babel（es5にトランスパイル）

```
npm install -D babel-loader @babel/core @babel/preset-env
```

```js
module: {
  rules: [
    {
      test: /\.js$/,
      use: [
        {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", {"modules": false}],
          },
        },
      ],
    },
  ],
},
```

[babel loader 使い方](https://www.npmjs.com/package/babel-loader)

#### ESlint（JS用静的検証ツール）

※JSに対するESlintは非検証。

```
npm install -D eslint eslint-loader
```

```js
{
  // enforce: 'pre'を指定することによって
  // enforce: 'pre'がついていないローダーより早く処理が実行される
  // 今回はbabel-loaderで変換する前にコードを検証したいため、指定が必要
  enforce: 'pre',
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'eslint-loader'
}
```

### TypeScript

TypeScript 使用には設定ファイル（tsconfig.json）作成要。設定項目等は以下見れば良さそう

[Typescript の tsconfig.json の簡単作成方法と設定について](https://usefuledge.com/pb_00009_tscconfig_json.html)

- tsconfig.json 簡易例

```json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5", // TSはECMAScript 5に変換
    "module": "es2015" // TSのモジュールはES Modulesとして出力
  }
}
```

#### awesome-typescript-loader

```
npm i -D typescript awesome-typescript-loader
```

```js
module: {
  rules: [
    {
      test: /\.tsx?$/,
      exclude: /node_modules/,
      use: "awesome-typescript-loader",
    },
  ];
},
resolve: {
  // 拡張子を配列で指定
  extensions: [".ts", ".tsx", ".js"],
},
```

#### ts-loader

※ts-loader より awesome-typescript-loader の方が速いとか

```
npm i -D typescript ts-loader
```

```js
module: {
  rules: [
    {
      test: /\.ts$/,
      exclude: /node_modules/,
      use: "ts-loader",
    },
  ],
},
resolve: {
  // 拡張子を配列で指定
  extensions: [".ts", ".js"],
},
```

#### TSLint（TS用静的検証ツール）非推奨

```
npm i -D tslint tslint-loader
```

```js
module: {
  rules: [
    {
      enforce: "pre",
      test: /\.tsx?$/,
      use: [
        {
          loader: "tslint-loader",
          options: {
            typeCheck: true,
            fix: true,
          },
        },
      ],
    },
  ];
}
```


#### ESLint

TypeScriptでもESLintの使用推奨

```
npm install -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-config-prettier eslint-plugin-prettier
```

webpackでの設定は、tslintとほぼ同じなため割愛

eslintの設定は .eslintrc.jsonに記述（以下サンプル）

```.eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "plugins": [
    "@typescript-eslint",
    "prettier"
  ],
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    "prettier/prettier": [
      "error",
      {
        /* format settings */
        "singleQuote": true,
        "semi": true,
        /* lint check settings */
        "max-len": [
          "warning",
          {
            "code": 100,
            "ignoreUrls": true
          }
        ],
        "quotes": [
          "warning",
          "single",
          {
            "avoidEscape": true,
            "allowTemplateLiterals": false
          }
        ]
      }
    ],
    /* lint ignore settings */
    "@typescript-eslint/no-non-null-assertion": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "@typescript-eslint/ban-types": 0
  }
}
```

・ESLint＋Pritter参考記事
- [脱TSLintして、ESLint TypeScript Plugin に移行する](https://qiita.com/suzuki_sh/items/fe9b60c4f9e1dbc5d903)
- [Prettierことはじめ](https://qiita.com/shinshin86/items/c8b1f6b22b1bef3074c7)

### Sass/CSS

scssファイルをjsにバンドルする流れ

>![sassをjsにバンドル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/3d15b6d8-4bfb-278f-98d2-795b1b4b1773.png)
>[最新版で学ぶ webpack 4 入門 スタイルシート(CSS/Sass)を取り込む方法](https://ics.media/entry/17376/)

#### Sass/CSS を JS にバンドル


```
npm i -D sass-loader sass style-loader css-loader
```
・style-loader : 動的に link タグに出力
・css-loader : css を JS にバンドル。css の url()や@import 文は js の require()に変換
・sass-loader : Sassをcssに変換する



※js に使用する css を import する必要あり

```js
// import 文を使ってstyle.cssファイルを読み込む。
import "./style.css";
```

- webpack.config.js

```js
module: {
  rules: [
    // Sassファイルの読み込みとコンパイル
    {
      test: /\.scss/, // 対象となるファイル名
      // ローダー名指定
      use: [
        "style-loader",
        // CSSをバンドルする機能
        {
          loader: "css-loader",
          options: {
            // CSS内のurl()メソッドを取り込むか否か
            url: false,
            // ソースマップの利用有無
            sourceMap: true,

            // 0 => no loaders (default);
            // 1 => postcss-loader;
            // 2 => postcss-loader, sass-loader
            importLoaders: 2,
          },
        },
        // Sassをバンドルするための機能
        {
          loader: "sass-loader",
          options: {
            // ソースマップの利用有無
            sourceMap: true,
          },
        },
      ],
    },
  ];
}
```

##### url-loader (全画像バンドル) 使用時

```js
module: {
  rules: [
    {
      test: /\.scss/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            // 要true
            url: true,
            sourceMap: true,
            importLoaders: 2,
          },
        },
        {
          loader: "sass-loader",
          options: {
            sourceMap: true,
          },
        },
      ],
    },
    {
      // 対象となるファイルの拡張子
      test: /\.(gif|png|jpg|eot|wof|woff|woff2|ttf|svg)$/,
      // 画像をBase64として取り込む
      loader: "url-loader",
    },
  ];
}
```

※一部の画像をバンドルしたくない場合 （url-loader で limit）

https://github.com/ics-creative/170330_webpack/tree/master/tutorial-sass-image-url-limit

※画像をバンドルせずそのまま配置したい場合 （file-loader）

https://github.com/ics-creative/170330_webpack/tree/master/tutorial-sass-image-file


##### ベンダープレフィックス自動付与したい場合

・ postcss-loader : css でネストした書き方をプレーンな css にする
・ css-loader と sass-loader の間に追加する

```js
module: {
  rules: [
    {
      test: /\.scss$/i,
      use: [
        {
          loader: MiniCssExtractPlugin.loader,
        },
        {
          loader: 'css-loader',
        },
        {
          loader: 'postcss-loader',
          options: {
            plugins: [
              require('autoprefixer')({
                //browsers: ["last 2 versions", "> 1%", "ie >= 10"],
                grid: true,
              }),
            ]
          }
        },
        {
          loader: 'sass-loader',
          options: {
            sassOptions: {
              outputStyle: 'expanded',
            },
          },
        },
      ]
    },
  ]
},
```

※package.json or .browserslistrc に autoprefixer / babel 等を使ってコンパイルするときに対応させるブラウザを指定

```json
{
  "browserslist": ["last 2 version", "> 1%", "ie >= 10"]
}
```

[.browserslistrc で対象ブラウザを指定する](https://qiita.com/takeshisakuma/items/0bc1c39ee976bd1f52d7)

## plugins プラグイン適用

### CSS を JS と分ける (MiniCssExtractPlugin)

- MiniCssExtractPlugin
  - CSS のバンドルを分ける（デフォルトは js に css もバンドルされる）
  - バンドルした JS から 任意のテキストを 別ファイルに出力できる

[mini-css-extract-plugin 公式](https://github.com/webpack-contrib/mini-css-extract-plugin)

```js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

plugins: [
  new MiniCssExtractPlugin({
    filename: 'bundle.css',  // 出力ファイル名
    path: path.join(__dirname, 'dist'),  // 出力先フォルダ
  })
],
```

- css + mini-css-extract-plugin の場合

```js
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        // CSSファイルを書き出すオプションを有効にする
        {
          loader: MiniCssExtractPlugin.loader,
        },
        // CSSを読み込む
        'css-loader',
      ],
    },
  ],
},
plugins: [
  new MiniCssExtractPlugin({
    filename: 'style.css',  // 出力ファイル名
  }),
],
```

- sass + mini-css-extract-plugin の場合

```js
module: {
  rules: [
    {
      test: /\.scss/, // 対象となるファイルの拡張子
      use:
        [
          // CSSファイルを書き出すオプションを有効にする
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
            options: {
              url: false,
              sourceMap: true,

              // 0 => no loaders (default);
              // 1 => postcss-loader;
              // 2 => postcss-loader, sass-loader
              importLoaders: 2,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              // ソースマップの利用有無
              sourceMap: true,
            },
          },
        ],
    },
  ],
},
plugins: [
  new MiniCssExtractPlugin({
    filename: 'style.css',  // 出力ファイル名
  }),
],
```

### HTML も コンパイルして配置 (HtmlWebpackPlugin)

htmlファイルに、`<script src="bundle.js">`や`<link href="style.css" rel="stylesheet">` 等を自動追加した上で、出力先フォルダに配置できるみたい

```
npm i -D html-webpack-plugin html-loader
```

```js
module: {
  loaders: [
    {
      test: /\.html$/,
      loader: "html-loader"
    }
  ]
},
plugins: [
  // index
  new HtmlWebpackPlugin({
    template: "./html/index.html"
  }),
  // その他
  new HtmlWebpackPlugin({
    filename: "admin.html",
    template: "./html/admin.html"
  })
]
```

## webpack モード指定実行

- package.json に以下指定

```json
{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production",
    "watch": "webpack --mode development --watch --color --progress"
  },
  :
}
```

- 実行コマンド

```
npm run dev
npm run build
npm run watch 
```

- webpack.config.js 

※development モードの時だけソースマップを入れるようにすることでデバッグ等で原因箇所を追いやすくする

```js
module.exports = (env, argv) => {
  const isDevMode = argv.mode === "development";

  return {
    mode: argv.mode,
    devtool: isDevMode ? "source-map" : "none",
    // 設定内容省略
  };
};
```

## TypeScript フォーマッター設定 (TSLint + Prettier)

```
npm i -D prettier tslint tslint-loader tslint-plugin-prettier tslint-config-standard tslint-config-prettier
```
・tslint-config-standard : JavaScript Standard Style を使用する
・tslint-plugin-prettier : TSLint を Prettier と連携する
・tslint-config-prettier : TSLint と Prettier のルール衝突回避(Prettier のルール優先)


- tslint.json

```json
{
  "rulesDirectory": ["tslint-plugin-prettier"],
  "extends": ["tslint-config-standard", "tslint-config-prettier"],
  "rules": {
    "prettier": [
      true,
      {
        "singleQuote": false,
        "semi": true
      }
    ]
  }
}
```

[rules.prettier の設定項目](https://prettier.io/docs/en/options.html)

- webpack.config.js の内容

```js
module: {
  rules: [
    {
      enforce: "pre",
      test: /\.tsx?$/,
      use: [
        {
          loader: "tslint-loader",
          options: {
            typeCheck: true,
            fix: true,
          },
        },
      ],
    },
    {
      test: /\.tsx?$/,
      exclude: /node_modules/,
      use: "awesome-typescript-loader",
    },
    {
      enforce: "pre",
      test: /\.js$/,
      use: "source-map-loader",
    },
  ];
}
```

TSLint と Prettier の衝突している設定確認したい場合

- 実行コマンド

```
npm run tslint-check
```

- package.json

```json
{
  "scripts": {
    "tslint-check": "tslint-config-prettier-check ./tslint.json"
  }
}
```

※ tsconfig.json に以下がないと、エラー「TS2307: Cannot find module 'source-map'.」が起きる

```json
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}
```

[解決方法](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/23649#issuecomment-424355987)

### TSLint + Prettier 情報元

詳細以下に記載あったため、必要あればご参照されたし（情報元に感謝を）

[TypeScript + TSLint + Prettier + Webpack 環境を作成したときのメモ](https://qiita.com/s4kr4/items/51809ff6670b49ef917a)

[TSLint を使う TypeScript プロジェクトにコードフォーマッタ Prettier を導入する](https://qiita.com/akisx/items/4b90106c7faca4965852)

# できたもの

[githubに一式置いているため必要あればご参照ください。](https://github.com/Symthy/TodoList-ts-pre/tree/record/first-webpack-introduction)

- srcフォルダ下は自身で作成したファイル群
- webpack実行(`npm run dev` or `npm run build`)でdistフォルダ下のファイル自動生成して配置される
  - tsファイル：自動フォーマット → jsに変換かつ es5にトランスパイル → bundle.js生成/配置
  - scss/cssファイル：scssをcssに変換 → style.css生成/配置
  - htmlファイル： `<script src="bundle.js">`, `<link href="style.css">`が追加された状態のindex.html生成/配置

```
.
├─dist
|  bundle.js  
|  index.html
|  style.css
└─src
    │  index.html
    │
    ├─css
    │  │  style.scss
    │  │
    │  ├─foundation
    │  │    normalize.css
    │  │
    │  ├─layout
    │       layout.scss
    │  
    └─ts
        │  app.ts
        │
        ├─model
             user.ts

```

# 参考サイト一覧

[webpack の基本だけどハマりやすい entry の設定と[name]](https://qiita.com/sansaisoba/items/921438a19cbf5a31ec53)

[webpack の基本的な使い方まとめ](http://var.blog.jp/archives/79399776.html)

[Webpack 4 + Babel + Sass 環境を構築](https://www.white-space.work/webpack-4-settings/)

[最新版で学ぶ webpack 4 入門 JavaScript のモジュールバンドラ](https://ics.media/entry/12140/)

[webpack 入門](https://qiita.com/soarflat/items/28bf799f7e0335b68186)

[TypeScript+webpack 4 の環境構築まとめ(React, Vue.js, Three.js のサンプル付き)](https://ics.media/entry/16329/#webpack-ts)

[最新版で学ぶ webpack 4 入門 スタイルシート(CSS/Sass)を取り込む方法](https://ics.media/entry/17376/)

[webpack で html ファイルも出力する](https://ema-hiro.hatenablog.com/entry/2017/10/12/015748)

[webpack チートシート](https://qiita.com/morrr/items/0f35adc38b59f5a67141)

[【2019 年版】webpack 4 個人的設定まとめ](https://www.expexp.jp/webpack4-2019/#config-babel)