🦔

いちばんやさしい webpack 入門

2022/05/02に公開
2

webpack is 何?

webpack とは、一言で言うと JavaScript 向けのモジュールバンドラーです。
複数の JavaScript モジュールを一つ(またはいくつか)のファイルへバンドル(=bundle: 束にする、包む)してくれます。

https://webpack.js.org/


複数の JS モジュールを(場合によっては CSS や画像などのアセット類も)一つにまとめる

すでに新規開発の終了も伝えられる webpack ですが、「STATE OF JS 2022」ではいまだに利用率 No.1 の地位にあります。

webpack 後継のモジュールバンドラーとしては、すでに Turbopack の開発開始がアナウンスされています。しかし、これがプロダクションレベルに達するまでは webpack がおそらく使い続けられることになるでしょう。

使うメリットは何?

  • モジュールを 1 つ(もしくは少数)にまとめることでブラウザからのリクエスト数を減らし、ファイル転送の効率が向上します。
  • ES Modules や CommonJS 形式のモジュールなど、さまざまな形式のモジュールに対応しています。
  • 上記の JS モジュールのみならず、CSS や画像ファイルもバンドルすることができます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules

とりあえず webpack を使ってみる

https://nodejs.org/

https://code.visualstudio.com/

1. プロジェクト・フォルダの作成

"zenn-webpack" という名前のフォルダを作成し、Node.js のプロジェクトとして初期化します。

bash
mkdir zenn-webpack
cd zenn-webpack

# Node.js プロジェクトとして初期化
npm init --yes

https://docs.npmjs.com/cli/v8/commands/npm

2. webpack のインストール

プロジェクトに webpack をインストールします。

  • 本体
bash
npm install --save-dev webpack
  • webpack を CLI から利用するために必要なツール
bash
npm install --save-dev webpack-cli

3. ソースファイルの用意

バンドル前の hello.js モジュールと、そのモジュールを利用する index.js スクリプトを src フォルダ内へ作成します。

src/hello.js
export class Hello {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, ${this.name}!`);
  }
}
src/index.js
import { Hello } from "./hello";

new Hello("taro").greet();

ここまででプロジェクトフォルダの構成は以下のようになりました。

% tree -I 'node_modules'
.
├── package-lock.json
├── package.json
└── src
    ├── hello.js
    └── index.js

1 directory, 4 files

4. バンドルの実行

webpack はデフォルトで、src/index.js(=エントリーファイル)とそこから読み込まれているモジュールをバンドルして dist/main.js というファイルを出力します。

webpack を起動してバンドルを実行します。

  • バンドルの実行
bash
./node_modules/.bin/webpack
  • または npx コマンド(npm に同梱)を利用する
bash
npx webpack
asset main.js 127 bytes [emitted] [minimized] (name: main)
orphan modules 138 bytes [orphan] 1 module
./src/index.js + 1 modules 218 bytes [built] [code generated]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

webpack 5.72.0 compiled with 1 warning in 102 ms

dist フォルダが作成され、main.js というバンドルファイルが出力されました。

  .
+ ├── dist
+ │   └── main.js
  ├── package-lock.json
  ├── package.json
  └── src
      ├── hello.js
      └── index.js

5. ローカルサーバと Watch モードの利用

では、上で出力された dist/main.js を読み込む HTML ファイルを作成し、それをローカルサーバでサーブしてみます。

dist/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Webpack</title>
  </head>
  <body>
    <script src="./main.js"></script>
  </body>
</html>
.
├── dist
│   ├── index.html
│   └── main.js
├── package-lock.json
├── package.json
└── src
    ├── hello.js
    └── index.js

ローカルサーバには、VS CodeLive Server 拡張機能を利用します。

https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer

この拡張機能をインストールしたら、VS Code でプロジェクトフォルダ内の dist/index.html を開いた状態でステータスバーの 📡 "Go Live" をクリックします。

ブラウザで localhost:5500 が開かれるので、デベロッパーコンソール(※)を確認すると Hello, taro! と出力されているはずです。

webpack にはファイルの変更を検知して自動的に再バンドルする watch モードも用意されています。
--watch オプションを付加してもう一度バンドルを実行してみましょう。

npx webpack --watch

ソースファイルを変更してみます。

src/index.js
  import { Hello } from "./hello";

- new Hello("taro").greet();
+ new Hello("Jiro").greet();


出力内容が Hello, Jiro! に代わった

6. バンドルファイルの確認

上で出力された dist/main.js の内容を見てみましょう(見やすいように整形済み)。

dist/main.js
(() => {
  'use strict';
  new (class {
    constructor(e) {
      this.name = e;
    }
    greet() {
      console.log(`Hello, ${this.name}`);
    }
  })('Jiro').greet();
})();

ソースファイルがそうなっているのだから当たり前ですが、このバンドルファイルでは class などの ES2015 以降の記法が使用されています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

https://thinkit.co.jp/article/10434/

つまり、このスクリプトは Internet Explorer やその他の古いブラウザでは実行できません

このような古い環境で動作させるには、ポリフィル/トランスパイラによる ES 記法への変換が必要となります。

webpack へ Babel を追加

Babel は JavaScript 用のトランスパイラ(ポリフィル)です。
ES6 (ES2015+) を ES5 へ変換するだけではなく、JSX などの非標準の JavaScript 構文を ES* へ変換することもできます。

https://babeljs.io/

https://ja.react.dev/learn/describing-the-ui

Babel の導入

このプロジェクトへ Babel を追加インストールして、webpack でのバンドル時にトランスパイルも併せて行えるようにします。

  • Babel 本体
bash
npm install --save-dev @babel/core
  • ES6 -> ES5 の変換に必要なプリセット
bash
npm install --save-dev @babel/preset-env

ローダーの導入

webpack では、JS の形式を変換したり、CSS(スタイルシート)やアセット画像などを JS へバンドルしたりするときにはローダーと呼ばれるプログラムを利用します。

変換の形式(jsx -> js, ts -> js など)やバンドルしたいファイル形式に応じて適切なローダーを webpack に利用させる必要があります。

ここでは Babel に「ES6 から ES5 へ」の変換をさせるので babel-loader をインストールします。

https://github.com/babel/babel-loader#readme

bash
npm install --save-dev babel-loader

設定ファイル .babelrc の作成

Babel の設定はプロジェクトフォルダ直下の .babelrc ファイルへ記述します。
ここでは上でインストールしたプリセットを指定するだけです。

.babelrc
{
  "presets": ["@babel/preset-env"]
}

webpack.config.js を作成する

ここまではとくに何かを設定することもなく webpack を直接実行してきましたが、Babel を併せて利用するための設定ファイルを作成する必要が出てきました。

webpack の設定ファイルを JavaScript で記述する場合には CommonJS 形式を用います(その理由は本稿では省略します)。

https://ja.wikipedia.org/wiki/CommonJS

プロジェクトフォルダ直下へ webpack.config.js を作成します。

webpack.config.js
/** ↓ エディタで補完を効かせるための JSDoc */
/** @type {import('webpack').Configuration} */
const config = {
  module: {
    rules: [
      {
        // 拡張子 js のファイル(正規表現)
        test: /\.js$/,
        // ローダーの指定
        loader: "babel-loader",
      },
    ],
  },
};

// 設定を CommonJS 形式でエクスポート
module.exports = config;

変換やバンドルのルールは module.rules 配列に指定します。
多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになります。

ふたたび webpack を起動してバンドルしてみましょう。

bash
npx webpack

出力された dist/main.js (整形済み)を確認すると ES5 形式へ変換されていることが分かります。

dist/main.js(一部のみ)
(() => {
  "use strict";
  function e(e, n) {
    for (var t = 0; t < n.length; t++) {
      var r = n[t];
      (r.enumerable = r.enumerable || !1),
        (r.configurable = !0),
        "value" in r && (r.writable = !0),
        Object.defineProperty(e, r.key, r);
    }
  }

モードとソースマップ

モードの指定

ここまでバンドルしてきた中で、以下のような警告メッセージが表示されていました。

WARNING in configuration
Set 'mode' option to 'development' or 'production'
to enable defaults for each environment.

mode オプションを 'development'(開発モード)または 'production'(本番モード)の環境に応じて設定する必要があるようです。

production モードではスクリプト実行に必要のない部分(コメントや改行など)がすべて削除され、実行に適した形式へと最適化(ファイルサイズの縮減など)が行われます。

一方、development モードではこれらの最適化が行われないのでデバッグに適しています。

モードをコマンドラインから指定する場合は次のようにします:

npx webpack --mode development

設定ファイル webpack.config.js へ指定することもできます:

webpack.config.js
  const config = {
+   mode: "development",
    module: {
      rules: [

ソースマップ

ここでは Babel による ES6 から ES5 への変換を行なっているため、変換前後のコード対応表のようなものがあればデバッグに役立ちます。それがソースマップです。

webpack にソースマップも出力させるには devtool エントリを指定します。

webpack.config.js
  mode: "development",
  devtool: "source-map",  // または 'inline-source-map' など

ソースマップが存在する場合には、ブラウザの開発者ツールの Sources タブから変換前のソースコードを見ることができます。

webpack-dev-server でホットリロードしよう

ここまでは VS Code の拡張機能 LiveServer を利用してきましたが、ローカルサーバも webpack から立ち上げることができます。webpack-dev-server をインストールします。

bash
# npm install --save-dev と同義
npm i -D webpack-dev-server

デフォルトでは devServer はホストのルートディレクトリ / を起点として起動するため、サーブすべきディレクトリを webpack.config.js 内で指定してあげる必要があります。

ここでは index.html を置いているプロジェクト直下の ./dist を指定します。

webpack.config.js
  devtool: "source-map",
  devServer: {
    static: {
      directory: "./dist",
    },
  },

devServer の起動には serve を付け加えます。

bash
npx webpack serve --mode development

localhost:8080 をブラウザで開くとバンドル結果が表示されているはずです。

また、serve コマンドを用いて起動した場合には --watch オプションと同様に自動的にファイルへの変更が反映(=ホットリロード)されます。

JSX (React) もバンドルしてみる

1. Babel プリセットを追加

上で Babel は「JSX などの非標準の JavaScript 構文を ES* へ変換することもできます」と書きましたが、そのためには JSX -> JS 変換用のプリセットを追加する必要があります。

bash
npm i -D @babel/preset-react
.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

2. React のインストール

React をインストールします。

bash
# npm install --save と同義
npm i -S react react-dom

3. React アプリを準備

dist フォルダ内のバンドルファイルを削除し、src フォルダ内のファイルをシンプルな React アプリ (JSX) で置き換えます。

bash
rm dist/*.js* src/*.js
src/App.jsx
import React from "react";

export const App = () => {
  return (
    <div className="container">
      <h1>Hello.</h1>
    </div>
  );
};
src/index.jsx
import React from "react";
import { createRoot } from "react-dom/client";

import { App } from "./App";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

dist フォルダ内の index.html へ React アプリのマウントポイントを追加します。

dist/index.html
    <body>
+     <div id="root"></div>
      <script src="./main.js"></script>
    </body>
% tree -I 'node_modules'
.
├── dist
│   └── index.html
├── package-lock.json
├── package.json
├── src
│   ├── App.jsx
│   └── index.jsx
└── webpack.config.js

2 directories, 6 files

4. エントリーファイルと依存関係の解決

最初の方で「デフォルトで src/index.js(=エントリーファイル)とそこから読み込まれているモジュールをバンドルして」と書きましたが、このデフォルトの状態はもう使えなくなりました。

すでに src/index.js は存在せず、他のモジュールをインポートするエントリーファイルは src/index.jsx となったからです。

従前のバンドルのためのコマンドを実行すると次のようなエラーとなります。

bash
$ npx webpack serve --mode development

ERROR in main
Module not found: Error: Can't resolve './src' in '/Foo/zenn-webpack'

src でのモジュール解決(=resolve)が出来ません』とのメッセージ通り、モジュール間の依存関係の解決にどの種類のファイルを参照すべきなのかを明示的に webpack へ伝える必要が生じました。

これには resolve エントリーを用います。ここへ .jsx を加えることで webpack は src/index.jsx をエントリーファイルとして認識できるようになります。

webpack.config.js
module.exports = {
  // 依存関係解決に参照するファイルの拡張子を指定
  resolve: {
    extensions: [".js", ".json", ".jsx"],
  },

また、変換のルールも変更となったため module.rules を更新しなければなりません。

webpack.config.js
    module: {
      rules: [
        {
          // 拡張子 js または jsx のファイル(正規表現)
+         test: /\.jsx?$/,
-         test: /\.js$/,
          loader: "babel-loader",
        },
      ],
    },

再度 devServer を起動します。

bash
npx webpack serve --mode development


localhost:8080 で React アプリが動作

エントリーファイル名や出力ファイル名をカスタマイズする

上の resolve 設定によって src/index.jsx をエントリーファイルとすることが出来ましたが、index 以外のファイル名(たとえば main.jsx など)を使いたい場合もあるでしょう。

そういう場合は entry エントリーへそのファイル名を明示的に指定する必要があります。

webpack.config.js
  module.exports = {
    resolve: {
      extensions: [".js", ".json", ".jsx"],
    },
+   entry: "./src/main.jsx",

加えて、エントリーファイル同様に出力されるバンドルファイルの名前や出力先フォルダもカスタマイズすることができます。これには output エントリを用います。

webpack.config.js
// Node.js の path モジュールをインポート
const path = require("node:path");

module.exports = {
  entry: "./src/main.jsx",
  output: {
    // ファイル名
    filename: "bundle.js",
    // 出力するフォルダ
    path: path.resolve(__dirname, "dist"),
  },

ここでは node:path モジュールを使って出力フォルダの絶対パスを指定しています。

https://nodejs.org/api/path.html

entry でエントリーファイルにチャンク名を付けた場合には出力ファイルにもその名前を適用することができます。

webpack.config.js
  entry: {
    // チャンク名 "app"
    app: "./src/main.jsx",
  },
  output: {
    // [name] -> "app"
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
  },

複数のエントリーファイルがある場合などに自動的に出力名を割り振ってくれるので便利です。

プロダクションビルドする

本番環境にデプロイするためのビルドは、モードに production を指定し、serve コマンドを build へ置き換えるだけです。

bash
npx webpack build --mode production

NPM スクリプトを登録する

毎回ターミナルへ長いコマンドを打ち込むのも疲れてきたので、package.jsonscriptsNPM スクリプトを登録します。

https://docs.npmjs.com/cli/v8/using-npm/scripts

NPM スクリプトは npm run スクリプト名 で実行することができます。

package.json
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack build --mode production"
  },
bash
# 開発時
npm run dev

# プロダクションビルド
npm run build

NODE_ENV で処理を分岐する

ソースマップを作成するか否かなどの処理を環境変数 NODE_ENV の値によって分岐できるようにします。
Windows では NPM スクリプト(コマンドライン)に環境変数を付加するには cross-env パッケージのインストールが必要です。

bash
npm i -D cross-env
package.json
  "scripts": {
    "dev": "cross-env NODE_ENV=\"development\" webpack serve",
    "build": "cross-env NODE_ENV=\"production\" webpack build"
  },

※ macOS や Linux では cross-env は不要

webpack.config.js へ処理の分岐を書き加えます。

webpack.config.js
// development モードか否か?
const isDev = process.env.NODE_ENV === "development";

/** @type {import('webpack').Configuration} */
module.exports = {
  // モードの切り替え
  mode: isDev ? "development" : "production",
  // dev モードではソースマップをつける
  devtool: isDev ? "source-map" : undefined,
  entry: "./src/index.jsx",

CSS(スタイルシート)もバンドルしてみる

CSS ファイルも JS へバンドルしてしまうことが可能です。

webpack で CSS をバンドルするには、style-loadercss-loader の 2 つのローダーが必要です。

  • style-loader: <link /> タグへ CSS を展開します。
  • css-loader: CSS を JS へバンドルします。

ローダーは css-loader -> style-loader の順で適用する必要があります。

https://github.com/webpack-contrib/style-loader#readme

https://github.com/webpack-contrib/css-loader#readme

1. ローダーの追加インストール

bash
npm i -D style-loader css-loader

2. module.rules の追加

CSS 用のバンドルルールを module.rules 配列に追加します。
複数のローダーを適用する場合には loader の代わりに use 配列を用います。

webpack.config.js
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
      },
      {
        // 拡張子 css のファイル(正規表現)
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },

use 配列のローダーは配列の最後尾から順に適用されます。よって css-loader -> style-loader の順となります。

3. ソースファイルの用意

適当な CSS を styles.css へ記述して、それを App.jsx の中でインポートします。

src/styles.css
body {
  margin: 0;
}

.container {
  height: 100vh;
  display: grid;
  place-content: center;
  place-items: center;
}
src/App.jsx
  import React from "react";
+ import "./styles.css";

  export const App = () => {

4. devServer の起動

bash
npm run dev


スタイルシートが適用された

5. CSS にもソースマップを付ける

デバッグ時にはバンドル前の CSS ファイルへのソースマップも必要です。
options エントリを追加することでソースマップのオプションを設定することができます。

https://github.com/webpack-contrib/css-loader#options

webpack.config.js
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              // dev モードではソースマップを付ける
              sourceMap: isDev,
            },
          },
        ],
      },

Sass もバンドルする

Sass ファイルをバンドルするには sasssass-loader の追加が必要です。

  • sass: sass(scss) -> css の変換を行います。スタイルシート版 Babel のようなものです。
  • sass-loader: webpack に Sass を扱わせます。
bash
npm i -D sass sass-loader

ローダーの適用順は sass-loader -> css-loader -> style-loader となります。

webpack.config.js
        {
+         // 拡張子 scss または css のファイル
+         test: /\.s?css$/,
-         test: /\.css$/,
          use: [
            "style-loader",
            {
              loader: "css-loader",
              options: {
                sourceMap: isDev,
              },
            },
+           {
+             loader: "sass-loader",
+             options: {
+               sourceMap: isDev,
+             },
+           },
          ],
        },

ソースファイルの CSS を Sass(scss) へ変換し、前項同様に App.jsx でインポートします。

src/styles.scss
body {
  margin: 0;

  .container {
    height: 100vh;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
src/App.jsx
- import "./styles.css";
+ import "./styles.scss";

画像などのアセットファイルをバンドルする

webpack v5.x では別途ローダーを必要とせずに JS から読み込まれる画像やフォントなどのアセットファイルをバンドルできるようになりました。

アセットファイルのバンドルには module.rulestype エントリーを用います。

webpack.config.js
  module: {
    rules: [
      {
        // 画像やフォントファイル
        test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
        type: "asset",
      },


React の logo.svg をバンドル

type の値にはいくつかの選択肢がありますが、通常は以下の 3 つから選択することになります。

  • asset/inline: アセットを JS ファイルへバンドルします。
  • asset/resource: アセットを別ファイルとして出力します。
  • asset: アセットをバンドルするか、別のファイルとして出力するかを自動的に選択します。

アセットを JS ファイルへ含めてしまうとバンドルサイズが大きくなりすぎる(=読み込みに時間がかかる)場合のために別ファイルとして出力するオプションが用意されています。

このオプションを選択するケースには、ブラウザのリクエスト数を増やしてでもバンドルファイルの読み込みを早めるほうがメリットが大きい場合などが該当するでしょう。

アセット類の出力先を設定する

asset/resource を選択した場合のアセット類の出力先も output エントリでカスタマイズすることができます。これには assetModuleFilename を利用します。

webpack.config.js
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    // "dist/asset/名前.拡張子" として出力される
    assetModuleFilename: "asset/[name][ext]",
  },

上の filename エントリとは異なり、ドット . は不要であることに注意が必要です。

アセットのファイルサイズによってバンドルの可否を分ける

アセットファイルの容量によって自動的に asset/inlineasset/resource をアセットごとに使い分けることもできます。これには parser.dataUrlCondition.maxSize オプションを使います。

webpack.config.js
    rules: [
      {
        test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
	// type は自動モード
        type: "asset",
        parser: {
          dataUrlCondition: {
	    // 8kb 以上なら `asset/resource` する
            maxSize: 1024 * 8,
          },
        },
      },

HTML も webpack から出力する

これまでは dist ディレクトリに置いた HTML ファイルを devServer でサーブしていましたが、これも webpack から出力することができます。

html-webpack-plugin をインストールしましょう。

https://github.com/jantimon/html-webpack-plugin#readme

bash
npm i -D html-webpack-plugin

html-webpack-plugin はデフォルトで src/index.ejs をテンプレートとし、それにバンドル済みの JS を <script> ~ </script> タグとして差し込んだ HTML ファイルを出力します。

これ以外のファイル(たとえば *.html ファイル)をテンプレートにしたい場合には template オプションへ明示的にそのファイルを指定する必要があります。

webpack.config.js
// プラグインの読み込み
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // "plugins" エントリーを追加
  plugins: [
    // プラグインのインスタンスを作成
    new HtmlWebpackPlugin({
      // テンプレート
      template: "./src/index.html",
      // <script> ~ </script> タグの挿入位置
      inject: "body",
      // スクリプト読み込みのタイプ
      scriptLoading: "defer",
      // ファビコンも <link rel="shortcut icon" ~ /> として挿入できる
      favicon: "./src/favicon.ico",
    }),
  ],

index.htmldist から src フォルダへ移動させますが、<script> ~ </script> タグや <link rel="shortcut icon" ~ /> タグはプラグインが挿入してくれるため削除します。

src/index.html
    <body>
      <div id="root"></div>
-     <script src="./main.js"></script>
    </body>
  </html>

これにより前回のビルド結果は dist フォルダごと削除できるようになりました。

CSS も別ファイルとして出力する

CSP (Content-Security-Policy) の設定によってはインラインスタイルの使用が禁じられているような場合があります。

https://developer.mozilla.org/ja/docs/Web/HTTP/CSP

CSS を JS へバンドルすることはインラインスタイルに該当するため、これもアセット類と同様に別ファイルとして出力したいケースがあるでしょう。

これを実現するプラグインが mini-css-extract-plugin です。

https://github.com/webpack-contrib/mini-css-extract-plugin#readme

bash
npm i -D mini-css-extract-plugin

webpack.config.js で mini-css-extract-plugin を読み込み、style-loader に代わってこのプラグインのローダーを使用します。

webpack.config.js
// プラグインの読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
webpack.config.js
        {
          test: /\.s?css$/,
          use: [
-           "style-loader",
+           MiniCssExtractPlugin.loader,
            {
              loader: "css-loader",
              options: {
                sourceMap: isDev,
              },
            },

plugins 配列にこのプラグインのインスタンスを作成しておくことを忘れないでください。

webpack.config.js
    },
    plugins: [
+     new MiniCssExtractPlugin(),
      new HtmlWebpackPlugin({
        template: "./src/index.html",
        inject: "body",
        scriptLoading: "defer",
      }),
    ],
    devtool: isDev ? "source-map" : undefined,
    devServer: {
      static: {
        directory: "./dist",
      },
    },
  };

dist フォルダへ CSS ファイルも出力されるようになりました。

上の html-webpack-plugin利用しない場合、HTML ファイルへ手動で <link rel="stylesheet" ~ /> としてインポートする必要があります。

dist/index.html
    <head>
      <meta charset="UTF-8" />
      <title>Webpack</title>
+     <link rel="stylesheet" href="main.css" />
    </head>

逆に html-webpack-plugin を利用する場合には、自動的に <link rel="stylesheet" ~ /> タグが HTML へ挿入されます。

ここまでの webpack.config.js

webpack.config.js(クリックで開閉)
webpack.config.js
const path = require("node:path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const isDev = process.env.NODE_ENV === "development";

/** @type {import('webpack').Configuration} */
const config = {
  mode: isDev ? "development" : "production",
  resolve: {
    extensions: [".js", ".json", ".jsx"],
  },
  entry: {
    main: "./src/index.jsx",
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    assetModuleFilename: "asset/[name][ext]",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
      },
      {
        test: /\.s?css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: isDev,
            },
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
        type: "asset/resource",
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      inject: "body",
      scriptLoading: "defer",
    }),
  ],
  devtool: isDev ? "source-map" : undefined,
  devServer: {
    static: {
      directory: "./dist",
    },
  },
};

module.exports = config;

TypeScript をバンドルする

1. 必要なパッケージのインストール

  • TypeScript 本体
bash
npm i -D typescript
  • ローダー
bash
npm i -D ts-loader

2. tsconfig.json の作成

TypeScript の挙動を規定する tsconfig.json を作成します。
また、React を利用する場合には "jsx": "react" の追記も必要です。

bash
npx tsc --init

tsconfig.json(コメントは削除)
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "jsx": "react"
  }
}

3. webpack.config.js のアップデート

TS のバンドルには babel-loader の代わりに ts-loader を利用します。

(変更部分のみを抜粋)

webpack.config.js
const config = {
  entry: {
    // "tsx" へ変更
    main: "./src/index.tsx",
  },
  resolve: {
    // TS ファイルを追加
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        // "tsx" へ変更
        test: /\.tsx?$/,
        // "babel-loader" -> "ts-loader"
        loader: "ts-loader",
      },
    ],
  },
};

webpack.config を TypeScript で記述する

webpack.config.js の拡張子を ts へ変更するには以下の 2 つのパッケージが必要です。

  • ts-node: TypeScript のまま Node.js スクリプトを実行できるようにするモジュール
  • @types/node: TypeScript 用 Node.js の型定義ファイル
sh
npm i -D ts-node @types/node

また、tsconfig.jsonts-node セクションでは "module": "CommonJS" を指定する必要があります。

tsconfig.jsonの例
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react"
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}
webpack.config.ts の例
webpack.config.ts
// require 文から import 文へ
import path from "node:path";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";

/** 以下 2 行は補完を効かせるためのインポート */
import "webpack-dev-server";
import { Configuration } from "webpack";

const isDev = process.env.NODE_ENV === "development";

const config: Configuration = {
  mode: isDev ? "development" : "production",
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
  },
  entry: {
    main: "./src/index.tsx",
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    assetModuleFilename: "asset/[name][ext]",
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
      },
      {
        test: /\.s?css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: isDev,
              importLoaders: 1,
            },
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
        type: "asset/resource",
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      inject: "body",
      scriptLoading: "defer",
    }),
  ],
  devtool: isDev ? "inline-source-map" : undefined,
  devServer: {
    static: {
      directory: "./dist",
    },
  },
};

// 設定をデフォルトエクスポート
export default config;

公式ドキュメント

https://webpack.js.org/concepts/

Discussion

rukaokamotorukaokamoto

分かりやすい記事をありがとうございます。typo見つけたので報告です。

プロジェクトフォルダ直下へ webpack.confg.js を作成します。
↓
プロジェクトフォルダ直下へ webpack.config.js を作成します。