webpack5でPug + Sass + TypeScriptのビルド環境を作る

webpack5でPug + Sass + TypeScriptのビルド環境を作る

今回はwebpack5の設定方法について備忘録として残しておきたいと思います。

設定する内容は以下です。

  • Pugのコンパイル
  • Sassのコンパイル(Dart Sass)
  • JavaScriptをBabelでトランスパイル
  • TypeScriptのコンパイル
  • 画像の圧縮
  • 開発サーバーの立ち上げ、ホットリロード
  • ESLintとPrettier

バージョンは以下です。

  • macOS Catalina v10.15.7
  • Visual Studio Code v1.67.1 
  • webpack v 5.72.0
  • node.js v17.7.2

また、今回作成した開発環境は以下のリポジトリの方に上げさせてもらいましたので非推奨の書き方やより良い方法などありましたら教えてもらえると嬉しいです。

GitHubリポジトリ

ディレクトリ構成

srcフォルダで作業したものが、distフォルダ直下に出力されます。

また、参考までにPugとSassのディレクトリ構成も載せておきます。

pugフォルダの中身

pug/
├─ data/
│  └─ _page-data.pug(meta情報などを管理する)
│
├─ modules/
│  ├─ _header.pug
│  ├─ _footer.pug
│  ├─ _sidebar.pug
│  ├─ _template.pug
│  
├─ lower-page/(下層ページ: 名前は任意)
│  ├─ _index.pug
│   
└─ index.pug(トップページ)

meta情報の管理などに関しては以下の文献を参考にさせていただきました。

pugでmeta情報などをjsonで外部ファイル化して読み込む方法

scssフォルダの中身

css設計、命名規則はFROCSSをベースにしています

scss/
├─ global/
│  ├─ mixin/(mixinの管理)
│  │  ├─ _breakpoint.scss
│  │  ├─ _font-size.scss
│  │  ├─ _line-height.scss
│  │  └─ _index.scss
│  │
│  ├─ variables/(グローバル変数の管理)
│  │  ├─ _color.scss
│  │  ├─ _font-family.scss
│  │  ├─ _z-index.scss
│  │  └─ _index.scss
│  │
│  └─ _index.scss(mixinとvariablesフォルダを読み込む用)
│
├─ foundation/(デフォルトスタイルを管理)
│  ├─ _base.scss
│  └─ _ress.scss(リセットCSS)
│  
├─ layout/(レイアウト部分の管理)
│  ├─ _l-container.scss
│  ├─ _l-header.scss
│  ├─ _l-footer.scss
│  └─ _l-sidebar.scss
│
├─ component/(最小単位のコンポーネントを管理)
│  └─ _c-button.scss
│
├─ Project/(いくつかのcomponentと、他の要素によって構成される大きな単位のオブジェクトを管理)
│  ├─ _p-global-nav.scss
│  └─ _p-main-visual.scss
│
├─ utility/(ユーティリティクラスの管理)
│  └─ _u-hidden.scss
│
├─ javascript/(jsで操作するDOMの管理)
│  ├─ _js-trigger.scss
│  └─ _js-target.scss
│
├─ external/(ライブラリなど外部クラスを上書き用)
│  └─ _swiper.scss
│
└─ style.scss

webpackのインストール

npm init -yでpackage.json
npm i -D webpack webpack-cliでwebpackのインストールします。


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

パッケージのインストール

各種ローダー、プラグインをインストールします。


$ npm i -D babel-loader @babel/preset-env @babel/core core-js 
$ npm i -D @babel/preset-typescript 
$ npm i -D typescript 
$ npm i -D ts-loader 
$ npm i -D html-webpack-plugin 
$ npm i -D pug 
$ npm i -D pug-html-loader 
$ npm i -D html-loader 
$ npm i -D mini-css-extract-plugin 
$ npm i -D css-loader 
$ npm i -D style-loader 
$ npm i -D sass 
$ npm i -D sass-loader 
$ npm i -D globule 
$ npm i -D eslint eslint-config-prettier 
$ npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser 
$ npm i -D prettier 
$ npm i -D image-webpack-loader 
$ npm i -D autoprefixer 
$ npm i -D postcss-loader
$ npm i -D clean-webpack-plugin
$ npm i -D webpack-dev-server

それぞれのバージョンは以下です。

webpack.config.jsの設定

webpack.config.jsを作成し、設定を記述します。

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const globule = require("globule");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// 本番環境のときはsoucemapを出力させない設定
const enabledSourceMap = process.env.NODE_ENV !== "production";

const app = {
  //エントリーポイント
  entry: './src/js/main.js',
  // 出力先(distの中のjsフォルダへbundle.jsを出力)
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: "./js/bundle.js",
  },
  //仮想サーバーの設定
  devServer: {
    //ルートディレクトリの指定
    static: {
      directory: path.join(__dirname, "dist")
    },
    compress: true,
    // ブラウザを自動的に起動
    open: true,
    // ホットリロード
    hot: true,
    // ポート番号指定
    port: 3000,
    // 監視するフォルダ
    watchFiles: {
      paths: ["src/**/*"],
    },
    // bundle先ファイルを出力する
    devMiddleware: {
      writeToDisk: true,
    }
  },
  module: {
    rules: [
      {
        //babelの設定
        test: /\.(ts|js)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    // 必要なポリフィルを出力させる
                    useBuiltIns: 'usage',
                    corejs: {
                      version: '3.22.5',
                      proposals: true
                    }
                  }
                ],
                ['@babel/preset-typescript']
              ]
            }
          },
        ]
      },
      {
        //Sassの設定
        test: /\.(sa|sc|c)ss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
            options: {
              url: false,
              sourceMap: enabledSourceMap,
              importLoaders: 2
            }
          },
          {
            loader: "postcss-loader",
            options: {
              // production モードでなければソースマップを有効に
              sourceMap: enabledSourceMap,
              postcssOptions: {
                // ベンダープレフィックスを自動付与
                plugins: [require("autoprefixer")({ grid: true })]
              }
            }
          },
          {
            loader: 'sass-loader',
            options: {
              // dart-sass を優先
              implementation: require("sass"),
              //  production モードでなければソースマップを有効に
              sourceMap: enabledSourceMap
            }
          },
        ]
      },
      {
        //画像の設定
        test: /\.(jpe?g|png|gif|svg|webp)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'img/[name][ext]',
        },
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              webp: {
                quality: 75
              }
            }
          }
        ]
      },
      {
        // Pugの設定
        test: /\.pug$/,
        use: [
          {
            loader: 'html-loader'
          },
          {
            loader: 'pug-html-loader',
            options: {
              pretty: true
            }
          }
        ]
      }
    ]
  },
  resolve: {
    // import 文で .ts ファイルを解決
    extensions: [".ts", ".js"]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename:'./css/style.css',
    }),
  ],
  //source-mapを出力する
  devtool: "source-map",

  // node_modules を監視(watch)対象から除外
  watchOptions: {
    ignored: /node_modules/ 
  }
}
//srcフォルダからpugを探す
const templates = globule.find("./src/pug/**/*.pug", {
  ignore: ["./src/pug/**/_*.pug"]
});

//すべてのpugファイルをhtmlに変換
templates.forEach((template) => {
  const fileName = template.replace("./src/pug/", "").replace(".pug", ".html");
  app.plugins.push(
    new HtmlWebpackPlugin({
      filename: `${fileName}`,
      template: template,
      inject: true, 
      minify: false //本番環境でも圧縮するか
    }),
  );
});

module.exports = app;

tsconfig.jsonを設定

TypeScriptからJavaScriptへのコンパイルオプションの指定をtsconfig.jsonに書きます。

以下のコマンドでtsconfig.jsonが作成されます。

$ npx tsc --init

tsconfig.jsonにて、設定を記述します。

{
  "compilerOptions": {
    "target": "ES5", // // ECMAScript ターゲットのバージョンを指定
    "module": "ES2015", // ES Modulesとして出力
    "sourceMap": true, // soucemapを出力する
    "strict": true, // 厳格モード
    "moduleResolution": "node", // node_modules からライブラリを読み込む
    "esModuleInterop": true, // CommonJSモジュールとESモジュール間の相互運用性を,すべてのインポート用に名前空間オブジェクトを作成することで可能に
    "skipLibCheck": true, // 型宣言ファイルの型チェックをスキップする
    "forceConsistentCasingInFileNames": true, // 大文字小文字を区別して参照を解決するようにする
    "noImplicitAny": false // 暗黙のany型をエラーに
  }
}

設定方法は以下の文献を参考にしました。

ESLintの設定

eslintrc.jsonを用意して設定を記述します。

// eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "root": true,
}

.eslintignoreにESLintでチェックしなくて良いフォルダやファイルを指定します。

// .eslintignore
node_modules/
dist/
webpack.config.js

以下、細かい設定に関する参考文献です。

また、今回はvscodeの拡張機能から利用しています。
以下をインストールして有効化します。

webpack上でESLintを使用したい場合は以下の記事が参考になりました。

Prettierの設定

.prettierrc.jsonを用意し、設定を記述します。

{
  "printWidth": 120, // 折り返す行の長さを指定
  "trailingComma": "es5", // 末尾のカンマの設定、ES5で有効な末尾のカンマ(オブジェクト、配列など) デフォルト
  "tabWidth": 2, // インデントのスペースの数を指定
  "semi": true, //最後にセミコロンを追加(デフォルト)
  "singleQuote": true,  // シングルクォートを使う
  "endOfLine": "lf" //改行コード、一般的なラインフィード(\n)のみ
}

設定に関しては以下の記事を参考にしました。

また、こちらもESLint同様、VSCode拡張機能をインストールして、有効化します。

VSCode上の設定

VSCode上でcommand + shift + P(Windows: Ctrl + Shift + P)でコマンドパレットを表示し、

settingsまたは設定(日本語化済みの場合)と入力し、settings.jsonを開き以下を追加します。

"editor.defaultFormatter": "esbenp.prettier-vscode", // フォーマッタ-はPrettierを使用
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true // ファイル保存時に ESLint でフォーマット
  },
"editor.formatOnSave": true, //ファイルを保存したタイミングでフォーマットする

ファイルを保存したタイミングで、ESLintとPrettierが機能するようになります。

Autoprefixerの設定

今回の設定項目は以下です。

  • 0.2% 以上のシェアがあり、メンテナンスが行われているブラウザ
  • 対象ブラウザから IE11 を除外

※IEは2022 年 6 月 16 日(日本時間)にサポートが終了します。

package.jsonに以下を追加します。

"browserslist": [
    "> 0.2%, not dead",
    "not IE 11"
  ]

webpack.config.jsの詳細

エントリーポイントと出力先を指定する

// エントリーポイント
  entry: './src/js/main.js',
  // 出力先(distの中のjsフォルダへbundle.jsを出力)
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: "./js/bundle.js",
  },

src/jsフォルダの中にあるmain.jsまたは.tsを読み込み、dist/jsフォルダへbundle.jsという名前で出力させています。

複数のエントリーポイントを指定する場合

entryのパスを複数定義し、outputでは[name]+パスを指定します。

entry: {
  front: './src/js/main.js',
  page: './src/js/page-main.js',
},
output: {
  filename: './js/[name].bundle.js',
  path: path.join(__dirname, 'dist'),
},

この状態でコマンドを実行すると、outputで指定した場所とファイル名で出力がされます。

Pugの指定

ローダーを指定します。

  • pug-html-loader => pugをhtmlに変換
  • html-loader => HTMLを文字列として出力

注意点としてはローダーは基本下から順番に処理が走ります。

{
  // Pugの設定
  test: /\.pug$/,
  use: [
    {
      loader: 'html-loader'
    },
    {
      loader: 'pug-html-loader',
      options: {
        pretty: true
      }
    }
  ]
}

反復処理ですべてのpugファイルをhtmlに変換します。

//srcフォルダからpugを探す
const templates = globule.find("./src/pug/**/*.pug", {
  ignore: ["./src/pug/**/_*.pug"]
});

//すべてのpugファイルをhtmlに変換
templates.forEach((template) => {
  const fileName = template.replace("./src/pug/", "").replace(".pug", ".html");
  app.plugins.push(
    new HtmlWebpackPlugin({
      filename: `${fileName}`, // ファイル名を指定
      template: template, // どのフォルダから読み込むのか指定
      inject: true, // scriptタグの出力先
      minify: false //本番環境でも圧縮するか
    }),
  );
});

始めにに読み込んだHtmlWebpackPlugininjectオプションでscriptタグの出力先を選択できます。

  • true => headタグ内に出力(defer属性を付与)
  • false => 出力させない
  • ‘body’ => body終了タグの直前に出力

Sassの指定

ローダーを指定します。

  • sass-loader => SassをCSSに変換する
  • postcss-loader => ベンダープレフィックスを自動付与
  • css-loader => CSSをJavaScriptファイルに埋め込むためのローダー
  • MiniCssExtractPlugin => CSSを個別のファイルに抽出する
{
  //Sassの設定
  test: /\.(sa|sc|c)ss$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
    },
    {
      loader: 'css-loader',
      options: {
        url: false,
        sourceMap: enabledSourceMap,
        importLoaders: 2
      }
    },
    {
      loader: "postcss-loader",
      options: {
        // production モードでなければソースマップを有効に
        sourceMap: enabledSourceMap,
        postcssOptions: {
          // ベンダープレフィックスを自動付与
          plugins: [require("autoprefixer")({ grid: true })]
        }
      }
    },
    {
      loader: 'sass-loader',
      options: {
        // dart-sass を優先
        implementation: require("sass"),
        //  production モードでなければソースマップを有効に
        sourceMap: enabledSourceMap
      }
    },
  ]
},

MiniCssExtractPluginで抽出したCSSを、どのフォルダに何て名前で出力するか指定します。

plugins: [
  new CleanWebpackPlugin(),
  new MiniCssExtractPlugin({
    filename:'./css/style.css',
  }),
],

TypeScriptの指定

ts-loaderを使用する場合、以下を記述します。

{
  //ts-loaderを使用する場合の設定
  test: /\.ts$/,
  use: "ts-loader",
  exclude: /node_modules/
},

import 文で拡張子を省略するために以下を記述します。

resolve: {
    // import 文で .ts ファイルを解決
    extensions: [".ts", ".js"]
  },

また、ts-loader を使用しなくても以下のbabelの指定で@babel/preset-typescript’を使用してTypeScriptをコンパイルすることもできます。

babelの指定

  • @babel/preset-envを指定することでES6〜のコードをターゲットブラウザで動作するコードに変換
  • @babel/preset-typescriptを指定することでTypeScriptをJavaScriptにコンパイル
{
  //babelの設定
  test: /\.(ts|js)$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: [
          ['@babel/preset-env'], // ES2022 を ターゲットブラウザで動作するコードに変換
          ['@babel/preset-typescript'] // TypeScriptのコンパイル
        ]
      }
    },
  ]
},

package.jsonのbrowserslistの指定がターゲットブラウザとなります。

"browserslist": [
    "> 0.2%, not dead",
    "not IE 11"
  ]

また、core.jsuseBuiltInsを指定することで、browserslistに指定したターゲットブラウザに必要なポリフィルを出力させることができます。

{
  //babelの設定
  test: /\.(ts|js)$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              // 必要なポリフィルを出力させる
              useBuiltIns: 'usage',
              corejs: {
                version: '3.22.5',
                proposals: true
              }
            }
          ],
          ['@babel/preset-typescript']
        ]
      }
    },
  ]
},

公式ドキュメント

ローカルサーバーの指定

パッケージのwebpack-dev-serverから仮想サーバーを立ち上げることができます。

今回は以下を設定しました。

  • static => ルートディレクトリの指定
  • open => ブラウザを自動で立ち上げるかどうか
  • hot => ファイルが更新されたら、表示も変更させるか
  • port => ポート番号の指定
  • watchFiles => 監視するフォルダ
  • writeToDisk => developmentモードでもbundle先ファイルを出力する
devServer: {
    // ルートディレクトリの指定
    static: {
      directory: path.join(__dirname, "dist")
    },
    compress: true,
    // ブラウザを自動的に起動
    open: true,
    // ホットリロード
    hot: true,
    // ポート番号指定
    port: 3000,
    // 監視するフォルダ
    watchFiles: {
      paths: ["src/**/*"],
    },
    // bundle先ファイルを出力する
    devMiddleware: {
      writeToDisk: true,
    }
  },

公式ドキュメント

webpack-dev-server

画像の設定

webpack4までは、raw-loaderやurl-loader、style-loaderなどのローダーを使用してアセットファイルの依存関係を解決していましたが、
webpack5ではAsset Modulesのみで同様のことを実現できます。

Asset Modulesの設定する内容は以下です。

  • test => 指定した拡張子に対して処理を実行する。
  • type => アセットモジュールタイプの指定。個別にファイルを生成して、そのURLを出力する。
  • generator.filename => 出力先の指定。[name]や[ext]のプレースホルダーを使用できる。

また、image-webpack-loaderを指定してpng, jpeg, gif, svg, webp画像を圧縮します。

{
  //画像の設定
  test: /\.(jpe?g|png|gif|svg|webp)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'img/[name][ext]',
  },
  use: [
    {
      loader: 'image-webpack-loader',
      options: {
        // webpを有効にする
        webp: {
          quality: 75
        }
      }
    }
  ]
},

image-webpack-loaderでwebpを有効にする場合はoptionに追加する必要があります。

以下、公式ドキュメント。

image-webpack-loader

webpackを走らせる

package.jsonに任意のコマンドを登録して、webpackを走らせます。

以下は例です。

"scripts": {
  "build": "NODE_ENV=production webpack --mode production",
  "dev": "webpack --mode development",
  "server": "webpack serve --mode development",
},
$ npm run build // 納品時などに本番環境として出力
$ npm run dev // 開発環境として出力
$ npm run server // 仮想サーバー起動し開発環境として出力

参考文献

webpackの設定

TypeScriptの設定

ESLint Prettierの設定

Babelの設定