3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「2024年!初アウトプットをしよう」

【2024年版】Webpack×TypeScript×Pug×Stylusで作る爆速コーディング環境

Last updated at Posted at 2024-01-13

 制作の方針

  • webpack5を採用し汎用性の高い構成を採用しています。
  • Pug、Stylusを採用することで既存の関数を複数のプロジェクトで使用し効率化を図ります。
  • プロジェクトに参画しやすいようわかりやすい構成を意識しています。

ディレクトリ構成

今回以下の構成にします。
開発環境用はdistへ、本番環境用ファイルはprodへビルドを想定しています。

├─ src/
│  ├─ html/
│  │   └─ index.pug
│  ├─ static/
│  ├─ js/
│  │   └─ index.ts
│  ├─ stylus/
│  │   └─ index.styl
│  └─ images/
├─ dist/
├─ prod/
├─ package.json
├─ postcss.config.js
└─ tsconfig.json

事前準備

以下の設定ファイルを予め用意してnpm installを行います。

  • package.json
  • postcss.config.js
  • tsconfig.json

package.json

今回使用するライブラリをdevDependenciesに記載します。
開発用にはnpm run start
本番公開用にはnpm run build
を実行致します。

package.json
{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "config": {
    "path": "/"
  },
  "scripts": {
    "start": "webpack --watch --mode development",
    "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.4.16",
    "browser-sync-webpack-plugin": "^2.3.0",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.1",
    "globule": "^1.3.4",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.4",
    "html-webpack-pug-plugin": "^4.0.0",
    "image-webpack-loader": "^8.1.0",
    "mini-css-extract-plugin": "^2.7.6",
    "postcss-loader": "^7.3.3",
    "pug-html-loader": "^1.1.5",
    "stylus-loader": "^7.1.3",
    "terser-webpack-plugin": "^5.3.9",
    "ts-loader": "^9.5.1",
    "typescript": "^5.3.3",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4"
  }
}

postcss.config.js

postcss.config.js
module.exports = {
  plugins: [
    require("autoprefixer")({
      cascade: false,
      grid: "autoplace",
    }),
  ],
};

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "strict": true,
    "module": "es6",
    "moduleResolution": "node",
    "target": "es5",
    "jsx": "react"
  },
  "include": ["src/**/*"],
  "exclude": ["./dist/"]
}

こちらでnpm installを行います。

TypeScriptをコンパイル

まずはエントリーファイルとなるTypeScriptのコンパイルを実装致します。
こちらを実行すると/assets/js/bundle.jsが出力されます。

TerserPluginを使用して本番環境からconsole.logを削除しています。
fullhashで公開時はファイル名にハッシュ値を付与します。
※開発環境のみsource-mapの出力を行います。

webpack.config.js
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = (_, args) => {
  const isDevelopment = args.mode === "development";
  const outputPath = isDevelopment ? "dist" : "prod";
  const jsName = isDevelopment ? "bundle.js" : "bundle.[fullhash].js";

  const config = {
    entry: path.resolve(__dirname, "src/js/index.ts"),
    output: {
      path: path.resolve(__dirname, outputPath),
      publicPath: "/",
      filename: `assets/js/${jsName}`,
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: "ts-loader",
          exclude: /node_modules/,
        },
      ],
    },
    resolve: {
      extensions: [".tsx", ".ts", ".js"],
    },
    plugins: [],
    optimization: {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: isDevelopment ? false : true,
            },
          },
        }),
      ],
    },
  };
  
  if (isDevelopment) {
    config.devtool = "source-map";
  } else {
    config.devtool = false;
  }
  return config;
};

stylusをコンパイル

MiniCssExtractPluginを使用してビルドされるJavaScriptファイルからCSSファイルとして抽出します。

webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = (_, args) => {
  const cssName = isDevelopment ? "style.css" : "style.[hash].css";

  const config = {
    module: {
      rules: [
         {
          test: /\.styl$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: "css-loader",
              options: {
                url: true,
                import: true,
              },
            },
            "postcss-loader",
            "stylus-loader",
          ],
        },
      ],
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: `assets/css/${cssName}`,
      }),
    ],
    optimization: {
      minimizer: [
        new CssMinimizerPlugin(),
      ],
    },
  };
};

index.tsでindex.stylを読み込む

index.ts
import "../stylus/style.styl";

画像をコンパイル

デフォルトの設定ではビルド時にファイル名が変更されます。
そこでimage-webpack-loaderを使いディレクトリ構成とファイル名を変えずに最適化してコンパイルします。

webpack.config.js
module.exports = (_, args) => {
  const config = {
    module: {
      rules: [
         {
          test: /\.(png|jpe?g|gif|svg)$/i,
          type: "asset/resource",
          generator: {
            filename: (pathData) => {
              const newPath = pathData.filename.replace("src/", "");
              return `assets/${newPath}`;
            },
          },
          use: [
            {
              loader: "image-webpack-loader",
              options: {
                mozjpeg: {
                  progressive: true,
                  quality: 75,
                },
                optipng: {
                  enabled: true,
                  optimizationLevel: 7,
                },
                pngquant: {
                  quality: [0.65, 0.75],
                  speed: 4,
                },
                gifsicle: {
                  interlaced: false,
                  optimizationLevel: 1,
                },
                webp: {
                  quality: 75,
                },
              },
            },
          ],
        },
      ],
    },
  };
};

pugをコンパイル

まずはsrc/html/内のpugファイルをで先頭に_の無いファイルを除外して一覧で取得します。
※本番環境ではhtmlにprettyオプションでminify化しています。
dataオプションでpugファイルで使用する変数を渡しています。
今回はif(isDevelopment === false)で開発環境の条件分岐を行うことができます。

webpack.config.js
const globule = require("globule");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlWebpackPugPlugin = require("html-webpack-pug-plugin");


module.exports = (_, args) => {

  const config = {
    module: {
      rules: [
         {
          test: /\.pug$/,
          use: [
            "html-loader",
            {
              loader: "pug-html-loader",
              options: {
                pretty: isDevelopment ? true : false,
                basepath: path.resolve(__dirname, outputPath),
                data: {
                  isDevelopment,
                },
              },
            },
          ],
        },
      ],
    },
    plugins: [
      new HtmlWebpackPugPlugin({
        adjustIndent: true,
      }),
    ],
    optimization: {
      ...
    },
  };
  
  const pugFiles = globule.find("./src/html/**/*.pug", {
    ignore: ["./src/html/**/_*.pug"],
  });

  pugFiles.forEach((file) => {
    const filename = file.replace("./src/html/", "").replace(".pug", ".html");
    config.plugins.push(
      new HtmlWebpackPlugin({
        filename: filename,
        template: file,
        inject: "body",
        minify: false,
      })
    );
  });
  
  return config;
};

ローカルサーバーの設定

browser-sync-webpack-pluginを使用してローカルサーバーを立ち上げます。

webpack.config.js
const BrowserSyncPlugin = require("browser-sync-webpack-plugin");

module.exports = (_, args) => {

  const config = {
    module: {
      ...
    },
    plugins: [
      ...
    ],
    optimization: {
      ...
    },
  };

  if (isDevelopment) {
    config.plugins.push(
      new BrowserSyncPlugin({
        host: "localhost",
        port: 3000,
        server: { baseDir: [outputPath] },
        startPath: "/",
        files: ['./src/**/*.*'],
      })
    );
  }

  return config;

};

その他設定

その他ファイルのビルド

CopyFilePluginを使用してファビコンなどその他のファイルもビルドファイルに含めます。
今回src/static/に入れたファイルがビルドファイルのルートディレクトリにコピーされます。

ビルド時に既存のファイルを削除

clean-webpack-pluginを使用して既存ファイルを削除致します。
ハッシュ値の異なるビルドファイルが残ってしまうのを防ぎます。

webpack.config.js
const CopyFilePlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = (_, args) => {

  const config = {
    module: {
      rules: [
         {
          test: /\.pug$/,
          use: [
            "html-loader",
            {
              loader: "pug-html-loader",
              options: {
                pretty: isDevelopment ? true : false,
                basepath: path.resolve(__dirname, outputPath),
                data: {
                  isDevelopment,
                },
              },
            },
          ],
        },
      ],
    },
    plugins: [
      new CleanWebpackPlugin(),
      new CopyFilePlugin({
        patterns: [
          {
            from: "./src/static",
            to: path.resolve(__dirname, outputPath),
          },
        ],
      }),
    ],
    optimization: {
      ...
    },
  };

};

ビルドファイルの構成

最終的にdistprodそれぞれこの様な構成でビルドされます。

├─ dist/
│  └─ index.html
│  └─ assets/
│      ├─ css/
│      │   └─ style.css
│      ├─ js/
│      │   └─ bundle.js
│      └─ images/
│          └─ hogehoge.png
└─ prod/
   └─ index.html
   └─ assets/
       ├─ css/
       │   └─ style.[hash値].css
       ├─ js/
       │   └─ bundle.[hash値].js
       └─ images/
           └─ hogehoge.png

webpack.config

最終的に完成したwebpack.config.jsです。

webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

const globule = require("globule");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlWebpackPugPlugin = require("html-webpack-pug-plugin");

const CopyFilePlugin = require("copy-webpack-plugin");

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const BrowserSyncPlugin = require("browser-sync-webpack-plugin");

module.exports = (_, args) => {
  const isDevelopment = args.mode === "development";
  const outputPath = isDevelopment ? "dist" : "prod";
  const jsName = isDevelopment ? "bundle.js" : "bundle.[fullhash].js";
  const cssName = isDevelopment ? "style.css" : "style.[hash].css";

  const config = {
    entry: path.resolve(__dirname, "src/js/index.ts"),
    output: {
      path: path.resolve(__dirname, outputPath),
      publicPath: "/",
      filename: `assets/js/${jsName}`,
    },
    module: {
      rules: [
        {
          test: /\.styl$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: "css-loader",
              options: {
                url: true,
                import: true,
              },
            },
            "postcss-loader",
            "stylus-loader",
          ],
        },
        {
          test: /\.tsx?$/,
          use: "ts-loader",
          exclude: /node_modules/,
        },
        {
          test: /\.pug$/,
          use: [
            "html-loader",
            {
              loader: "pug-html-loader",
              options: {
                pretty: isDevelopment ? true : false,
                basepath: path.resolve(__dirname, outputPath),
                data: {
                  isDevelopment,
                },
              },
            },
          ],
        },
        {
          test: /\.(png|jpe?g|gif|svg)$/i,
          type: "asset/resource",
          generator: {
            filename: (pathData) => {
              const newPath = pathData.filename.replace("src/", "");
              return `assets/${newPath}`;
            },
          },
          use: [
            {
              loader: "image-webpack-loader",
              options: {
                mozjpeg: {
                  progressive: true,
                  quality: 75,
                },
                optipng: {
                  enabled: true,
                  optimizationLevel: 7,
                },
                pngquant: {
                  quality: [0.65, 0.75],
                  speed: 4,
                },
                gifsicle: {
                  interlaced: false,
                  optimizationLevel: 1,
                },
                webp: {
                  quality: 75,
                },
              },
            },
          ],
        },
      ],
    },
    resolve: {
      extensions: [".tsx", ".ts", ".js"],
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: `assets/css/${cssName}`,
      }),
      new HtmlWebpackPugPlugin({
        adjustIndent: true,
      }),
      new CleanWebpackPlugin(),
      new CopyFilePlugin({
        patterns: [
          {
            from: "./src/static",
            to: path.resolve(__dirname, outputPath),
          },
        ],
      }),
    ],
    optimization: {
      minimize: true,
      minimizer: [
        new CssMinimizerPlugin(),
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: isDevelopment ? false : true,
            },
          },
        }),
      ],
    },
  };

  const pugFiles = globule.find("./src/html/**/*.pug", {
    ignore: ["./src/html/**/_*.pug"],
  });

  pugFiles.forEach((file) => {
    const filename = file.replace("./src/html/", "").replace(".pug", ".html");
    config.plugins.push(
      new HtmlWebpackPlugin({
        filename: filename,
        template: file,
        inject: "body",
        minify: false,
      })
    );
  });

  if (isDevelopment) {
    config.plugins.push(
      new BrowserSyncPlugin({
        host: "localhost",
        port: 3000,
        server: { baseDir: [outputPath] },
        startPath: "/",
        files: ["./src/**/*.*"],
      })
    );
  }

  if (isDevelopment) {
    config.devtool = "source-map";
  } else {
    config.devtool = false;
  }

  return config;
};

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?