webpack を使って Sass をコンパイルする方法

Sasswebpack を使ってコンパイルする方法の覚書です。webpack は JavaScript だけではなく、CSS や Sass をバンドルすることができます。

以下は Node.js がインストールされていることを前提にしています。また、主なパッケージのバージョンは以下になります(環境は Mac での例になります)。

作成日:2020年11月9日

関連ページ:

webpack のインストール

webpack は Node.js のパッケージマネージャ npm を使ってインストールすることができます。

この例では作業ディレクトリ(myProject)を作成し、そこへローカルインストールを行います。

作業用フォルダーを任意の場所に作成しそこに移動します。

$ mkdir myProject  return //フォルダーを任意の場所に作成
        
$ cd myProject  return //作成したフォルダー(ディレクトリ)に移動

npm を使ってインストールする場合、まず npm init コマンドで package.json を生成します。デフォルトのオプションで package.json を生成すれば良いので -y オプションを指定します。

$ npm init -y return //package.json をデフォルトで生成

//生成された package.json のパスと内容が表示される
Wrote to /Applications/MAMP/htdocs/myProject/package.json:

{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

続いて npm install コマンドで webpack をインストールします。

コマンドライン操作用のパッケージは webpack-cli という別パッケージで提供されているので併せてインストールします(webpack 4.0以降)。

オプションの -D(--save-dev)は開発環境で使うパッケージに指定するオプションです。バージョンを指定しないでインストールすると最新版がインストールされます。

$ npm install -D webpack webpack-cli  return //webpack と webpack-cli をインストール
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN myProject@1.0.0 No description
npm WARN myProject@1.0.0 No repository field.

+ webpack-cli@4.2.0
+ webpack@5.4.0
added 136 packages from 155 contributors and audited 136 packages in 3.942s

13 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

インストールが完了すると、インストールされたパッケージは node_modules というフォルダに保存され、package.json 及び package-lock.json という npm の設定ファイルが生成されます。

$ tree -L 2  return //ツリー表示
.
├── node_modules  //npm でインストールされるパッケージのディレクトリ
│   ├── @types
│   ├── @webassemblyjs
│   ├── @webpack-cli
│
│    ・・・中略・・・
│
│   ├── webpack
│   ├── webpack-cli
│   ├── webpack-merge
│   ├── webpack-sources
│   ├── which
│   ├── wordwrapjs
│   └── wrappy
├── package-lock.json  //npm の設定ファイル
└── package.json  //npm の設定ファイル

webpack guides: installation

関連ページ:

初期構成の作成

初期構成として以下のようなフォルダとファイルを作成します。

ビルドを実行すると src フォルダのファイルがコンパイルされて dist フォルダに出力されます。

myProject
├── dist  //追加したフォルダ(空)
├── index.html  //追加した HTML ファイル(表示用)
├── node_modules
├── package-lock.json
├── package.json
└── src  //追加したフォルダ
       ├── index.js  //追加した Javascript ファイル(エントリーポイント)
       └── style.scss  //追加した Sass ファイル

エントリーポイントの index.js では import を使って同じ src フォルダの Sass ファイル(style.scss)を読み込みます。

index.js
// スタイルシートを読み込む
import "./style.scss";

Sass ファイルには確認用のサンプルを Sass を記述しています。

index.scss
$p_color: green;
$p_bg_color: yellow;
 
p {
  color: $p_color;
  background-color: $p_bg_color;
}

index.html では Sass がコンパイルされて出力される dist フォルダの CSS(まだこの時点では存在しない)を読み込みます。

MiniCssExtractPlugin というプラグインを使ってビルドを実行すると Sass ファイル(style.scss)をコンパイルして dist フォルダに出力することができます。この例では style.css という名前で CSS ファイルを出力します。

この例では Javascript ファイルはバンドルしませんが、Javascript もバンドルする場合は dist フォルダに main.js というファイルが出力されるのでそれを読み込みます。以下ではコメントアウトしていますが、webpack-dev-server を使ってライブリロードをするにはコメントアウトを外して読み込む必要があります。

index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="dist/style.css">
<title>Sass 読み込みサンプル</title>
</head>

<body>
<div class="sass_sample">
  <h1>Sass</h1>
  <p>sample text.</p>
</div>
<!--<script src="dist/main.js"></script>-->
</body>
</html>

ローダーとプラグインのインストール

以下の基本的なローダープラグインをインストールします。

  • css-loader:CSSを処理するためのモジュール
  • sass-loader:Sass を CSS へ変換するためのモジュール
  • sass:Sass をコンパイルするためのモジュール(Dart Sass
  • MiniCssExtractPlugin:CSS を別ファイルとして出力するためのプラグイン

Sass のモジュールは sass(Dart Sass)を使用しています。LibSass(Node Sass)は非推奨になるようです(LibSass is Deprecated)。

npm install コマンドに --save-dev オプションを指定してモジュールとプラグインをインストールします。

$ npm install --save-dev css-loader sass-loader sass mini-css-extract-plugin return
          
npm WARN myProject@1.0.0 No description
npm WARN myProject@1.0.0 No repository field.

+ mini-css-extract-plugin@1.3.0
+ sass@1.29.0
+ sass-loader@10.0.5
+ css-loader@5.0.1
added 44 packages from 44 contributors and audited 180 packages in 1.974s

19 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

上記インストールを実行後、package.json を確認すると devDependencies にインストールしたモジュールとプラグインがリストされています。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.0.1",
    "mini-css-extract-plugin": "^1.3.0",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

オプションで fibers をインストールします。

fibres パッケージを使用することでコンパイルの速度を速くすることができるようです。以下は sass-loader からの抜粋です。

sass(Dart Sass)を使用する場合、非同期コールバックのオーバーヘッドのため、同期コンパイルはデフォルトで非同期コンパイルの2倍の速度であることに注意してください。 このオーバーヘッドを回避するために、fibres パッケージを使用して、同期コードパスから非同期インポーターを呼び出すことができます。

fibers をインストール後の package.json は以下のようになっています。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "mini-css-extract-plugin": "^1.3.0",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js

webpack の設定ファイル webpack.config.js を作成します。

myProject
├── dist  
├── index.html 
├── node_modules
├── package-lock.json
├── package.json
├── src  
│   ├── index.js  
│   └── style.scss  
└── webpack.config.js   // webpack 設定ファイルを追加

以下が大まかなローダーの処理の流れです。

  • sass-loader を使って Sass を CSS へ変換
  • css-loader で CSS を JavaScript(CommonJS) に変換
  • MiniCssExtractPlugin.loader で CSS を 抽出して別ファイルとして出力

以下は webpack.config.js の例です。

webpack.config.js
//path モジュールの読み込み
const path = require('path');
//MiniCssExtractPlugin の読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {

  //エントリポイント(デフォルトと同じなので省略可)
  entry: './src/index.js',  
  //出力先(デフォルトと同じなので省略可)
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        // 対象となるファイルの拡張子(scss)
        test: /\.scss$/,
        // Sassファイルの読み込みとコンパイル
        use: [
          // CSSファイルを抽出するように MiniCssExtractPlugin のローダーを指定
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSSをバンドルするためのローダー
          {
            loader: "css-loader",
            options: {
              //URL の解決を無効に
              url: false,
              // ソースマップを有効に
              sourceMap: true,
            },
          },
          // Sass を CSS へ変換するローダー
          {
            loader: "sass-loader",
            options: {
              // dart-sass を優先
              implementation: require('sass'),
              sassOptions: {
                // fibers を使わない場合は以下で false を指定
                fiber: require('fibers'),
              },
              // ソースマップを有効に
              sourceMap: true,
            },
          },
        ],
      },
    ],
  },
  //プラグインの設定
  plugins: [
    new MiniCssExtractPlugin({
      // 抽出する CSS のファイル名
      filename: "style.css",
    }),
  ],
  //source-map タイプのソースマップを出力
  devtool: "source-map",
  // node_modules を監視(watch)対象から除外
  watchOptions: {
    ignored: /node_modules/  //正規表現で指定
  },
};
css-loader url オプション

上記の設定では css-loader の url オプションに false を指定して URL の解決を無効にしています。

この場合、スタイルシートで読み込む画像は出力先の dist 配下に配置します。

関連項目:画像を webpack の処理の対象外にする

myProject
├── dist
│   └── images  //画像を配置するフォルダ
│        └── sample.jpg   
├── index.html 
├── node_modules
├── package-lock.json
├── package.json
├── src  
│   ├── index.js  
│   └── style.scss  
└── webpack.config.js 

画像のパスは dist フォルダからのパス(./images/sample.jpg)になります。

.foo {
  background-image: url("./images/sample.jpg")
}

※ 後述の clean-webpack-plugin プラグインを使用すると、dist フォルダの中身はビルドする度にクリーンアップされてしまうため、file-loader を使用します。

npm script

package.json の scripts フィールドにコマンド(npm script)を追加して npm run コマンドで webpack を実行できるようにしておきます。

以下では build(ビルド)、dev(開発モード)、watch(監視モード)及び sass-ver(Sass のバージョン表示) のコマンドを追加しています。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "mini-css-extract-plugin": "^1.3.0",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

以下は Sass のバージョンを表示する例です。

$ npm run sass-ver  return 

> myProject@1.0.0 sass-ver /Applications/MAMP/htdocs/myProject
> sass --version

1.29.0 compiled with dart2js 2.10.3

コンパイル

npm script に登録した build コマンド(npm run build)を実行するとビルドすることができます。

上記を実行すると、dist フォルダに Sass ファイル(style.scss )がコンパイルされた CSS ファイル style.css が出力されます。

また、main.js という Javascript ファイルが出力されますが、この例では Javascript は使っていないので、main.js は空のファイルになります(webpack-dev-server を使うと関連の内容が記述されます)。

この例では、ソースマップを有効にしているので、style.css.map 及び main.js.map も生成されます。

myProject
├── dist //コンパイルされたファイルが出力される
│   ├── main.js
│   ├── main.js.map
│   ├── style.css
│   └── style.css.map  
├── index.html 
├── node_modules
├── package-lock.json
├── package.json
├── src  
│   ├── index.js  
│   └── style.scss  
└── webpack.config.js

npm run build

build コマンド(npm run build)の場合、style.css は以下のように outputStyle は compressed で出力されます。

style.css
p{color:green;background-color:#ff0}

/*# sourceMappingURL=style.css.map*/

npm run dev

dev コマンド(npm run dev)の場合、style.css は以下のように outputStyle は expanded で出力されます。

p {
  color: green;
  background-color: yellow;
}

/*# sourceMappingURL=style.css.map*/

npm run watch

watch コマンド(npm run watch)を実行すると、処理対象のファイルを監視してファイルが変更されると自動的に dvelopment モードでリビルド(再コンパイル)します。

dvelopment モードでリビルドするのは npm script で "watch": "webpack --mode development --watch" と登録しているためです。

watch モードを終了するには control + c を押します。

ソースマップの出力の制御

production モードのときはソースマップを出力しないようにする例です。

webpack を実行する際に、NODE_ENV にパラメータを渡して webpack.config.js で process.env.NODE_ENV で値を取得して production モードかどうかを判定します。

以下のように package.json の scripts フィールドに NODE_ENV を指定したビルドコマンドを追加します。この例では production モードを実行する build コマンドに NODE_ENV=production を追加します。

package.json 抜粋
"scripts": {
  "build": "NODE_ENV=production webpack --mode production",
  "dev": "webpack --mode development",
  "watch": "webpack --mode development --watch",
  "sass-ver": "sass --version"
},
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "mini-css-extract-plugin": "^1.3.0",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js で変数 enabledSourceMap を定義し、 production モード以外の場合に値を true に設定します。

そして sourceMap の設定で enabledSourceMap を指定し、production モード以外の場合にソースマップを有効にします。

webpack.config.js 抜粋
// production モード以外の場合、変数 enabledSourceMap は true
const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

 ・・・中略・・・

  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: "css-loader",
            options: {
              url: false,
              // production モードでなければソースマップを有効に
              sourceMap: enabledSourceMap,
            },
          },
          {
            loader: "sass-loader",
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
              },
              //  production モードでなければソースマップを有効に
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
    ],
  },
・・・以下省略・・・

上記により、npm run build を実行するとソースマップファイルは生成されず、npm run dev または npm run watch を実行するとソースマップファイルが生成されます。

※ 但し、このままでは npm run dev または npm run watch を実行して生成されたソースマップファイルはそのまま dist フォルダに残ってしまいます。

clean-webpack-plugin

clean-webpack-plugin を使うと各ビルドの前に distフォルダーをクリーンアップすることができます。

参考:Cleaning up the /dist folder

clean-webpack-plugin をインストールします。

webpack.config.js を編集してプラグイン clean-webpack-plugin の設定を追加します。

webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// CleanWebpackPlugin の読み込み
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  entry: './src/index.js',  
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: "css-loader",
            options: {
              url: false,
              sourceMap: enabledSourceMap,
            },
          },
          {
            loader: "sass-loader",
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
              },
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
    ],
  },
  //プラグインの設定
  plugins: [
    // CleanWebpackPlugin を追加
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
  ],
  devtool: "source-map",
  watchOptions: {
    ignored: /node_modules/
  },
};

これで毎回ビルドする度に distフォルダーはクリーンアップされるので、npm run dev または npm run watch を実行した後に npm run build を実行すると production モードで生成されたソースマップファイルは残りません。

※ 但し、css-loader の url オプションを false に指定して画像を dist フォルダに配置している場合、ビルドする度に画像のフォルダもクリーンアップされてしまいます。そのため、file-loader を使用して src フォルダに画像を配置してその画像をビルドの際に dist フォルダにコピーするようにします。

※※ また、watch モードを実行した際に何故か file-loader で画像がコピーされないので、 CleanWebpackPlugin のオプションで cleanStaleWebpackAssets: false を指定します(prodction 及び development モードではコピーされるのですが)。

file-loader を追加

file-loader を使うとスタイルシートで読み込んでいる画像などを、出力フォルダーにコピーして参照することができます。

file-loader を使う場合は src フォルダーに画像を配置して、file-loader によって画像が dist フォルダーにコピーされます。

myProject
├── dist 
│   ├── main.js
│   ├── style.css
│   └── images  //ビルド時にコピーされる
│        └── sample.jpg   
├── index.html 
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── images
│   │   └── sample.jpg  //背景画像を配置 
│   ├── index.js  
│   └── style.scss  
└── webpack.config.js 
.foo {
  background-image: url("./images/sample.jpg")
}

file-loader のモジュールを追加でインストールします。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "file-loader": "^6.2.0",
    "mini-css-extract-plugin": "^1.3.0",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js を編集します。

css-loader の url オプション(url: false)を削除します。

output に publicPath オプション(publicPath: '',)を追加します。このオプションを追加しないと「Error: Automatic publicPath is not supported in this browser」というエラーになりコンパイルできませんでした(webpack 5 から?)。

file-loader のローダーの設定(44〜58行目)を追加します。

また、CleanWebpackPlugin のオプションに cleanStaleWebpackAssets: false を追加します(64行目)。これは watch モードを実行した際に何故か file-loader で画像がコピーされないための対策で、画像は削除しないようにします。

cleanStaleWebpackAssets はリビルドの際に未使用のWebpackアセットをすべて自動的に削除するオプションでデフォルトは true です。

webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  entry: './src/index.js',  
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    // publicPath を追加
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: "css-loader",
            options: {
              //以下を削除
              //url: false,
              sourceMap: enabledSourceMap,
            },
          },
          {
            loader: "sass-loader",
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
              },
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
      // file-loader の設定を追加
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            //画像を出力フォルダーにコピーするローダー
            loader: 'file-loader',
            options: {
              // 画像ファイルの名前とパスの設定
              name: './images/[name].[ext]'
            }
          }
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin({
      //以下を追加
      cleanStaleWebpackAssets: false,
    }),
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
  ],
  devtool: "source-map",
  watchOptions: {
    ignored: /node_modules/
  },
};

postCSS

必要に応じて postCSS のプラグインをインストールします。

参考:webpack/ postcss-loader

プラグイン 説明
Autoprefixer ベンダープレフィックスを付与
CSS Declaration Sorter CSS プロパティをソート
PostCSS Sort Media Queries メディアクエリをまとめる

Autoprefixer

Autoprefixer を使うには postcss-loader もインストールする必要があります。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.0.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "file-loader": "^6.2.0",
    "mini-css-extract-plugin": "^1.3.0",
    "postcss-loader": "^4.0.4",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js に設定を追加します。

css-loader と sass-loader の間に postcss-loader を追加します(css-loader や style-loader の前に使用しますが、sass などの他のプリプロセッサローダーの後に使用します)。

development モードの場合は明示的にソースマップも有効にしています。

また、 css-loader の importLoaders オプションを追加して css-loader の前に適用されるローダーの数(2)を設定しています(この設定は将来的には変更になる可能性があるようです)。

webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  entry: './src/index.js',  
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: "css-loader",
            options: {
              sourceMap: enabledSourceMap,
              // postcss-loader と sass-loader の場合は2を指定
              importLoaders: 2, 
              // 0 => no loaders (default);
              // 1 => postcss-loader;
              // 2 => postcss-loader, sass-loader
            },
          },
          // PostCSS(autoprefixer)の設定
          {
            loader: "postcss-loader",
            options: {
              // PostCSS でもソースマップを有効に
              sourceMap: enabledSourceMap,
              postcssOptions: {
                // ベンダープレフィックスを自動付与
                plugins: ["autoprefixer"],
              },
            },
          },
          {
            loader: "sass-loader",
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
              },
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
      {
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: './images/[name].[ext]'
            }
          }
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,
    }),
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
  ],
  devtool: "source-map",
  watchOptions: {
    ignored: /node_modules/ 
  },
};

例えば、以下のような CSS を style.scss に記述してコンパイルすると、

::placeholder {
  color: gray;
}
 
.image {
  background-image: url(./images/sample.jpg);
}
@media (min-resolution: 2dppx) {
  .image {
    background-image: url(./images/sample.jpg);
  }
}

以下のようなベンダープレフィックスが付与されます(指定した画像が存在しない場合は css-loader によりエラーになります)。

::-moz-placeholder {
  color: gray;
}

:-ms-input-placeholder {
  color: gray;
}

::placeholder {
  color: gray;
}

.image {
  background-image: url(./images/sample.jpg);
}

@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
  .image {
    background-image: url(./images/sample.jpg);
  }
}

CSS Declaration Sorter

CSS プロパティをソートできるように css-declaration-sorter をインストールします。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.0.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-declaration-sorter": "^6.0.2",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "file-loader": "^6.2.0",
    "mini-css-extract-plugin": "^1.3.0",
    "postcss-loader": "^4.0.4",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js の postcss-loader に設定を追加します。order に指定できる値は以下になります。

  • alphabetical:アルファベット順(デフォルト)
  • smacss:最も重要なフローに影響を与えるプロパティから最も重要でないプロパティの順
    1. Box
    2. Border
    3. Background
    4. Text
    5. Other
  • concentric-css:ボックスモデルの外側に適用されるプロパティから内側の順
    1. Positioning
    2. Visibility
    3. Box model
    4. Dimensions
    5. Text
webpack.config.js 抜粋
{
  loader: "postcss-loader",
  options: {
    sourceMap: enabledSourceMap,
    postcssOptions: {
      plugins: [
        'autoprefixer',
        //CSS Declaration Sorter (アルファベット順でソート)
        ['css-declaration-sorter', { order: 'alphabetical' }],
      ],
    },
  },
},
webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  entry: './src/index.js',  
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: "css-loader",
            options: {
              sourceMap: enabledSourceMap,
              importLoaders: 2,
            },
          },
          // PostCSS
          {
            loader: "postcss-loader",
            options: {
              sourceMap: enabledSourceMap,
              postcssOptions: {
                plugins: [
                  'autoprefixer',
                  //CSS Declaration Sorter(アルファベット順でソート)
                  ['css-declaration-sorter', { order: 'alphabetical' }],
                ],
              },
            },
          },
          {
            loader: "sass-loader",
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
              },
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
      {
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: './images/[name].[ext]'
            }
          }
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,
    }),
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
  ],
  devtool: "source-map",
  watchOptions: {
    ignored: /node_modules/ 
  },
};

例えば、以下のような CSS を style.scss に記述してコンパイルすると、

.foo {
    display: block;
    z-index: 1;
    animation: none;
    color: #C55;
    border: 0;
}

以下のようにアルファベット順にソートされます。また、この例ではベンダープレフィックスも付与されています。

.foo {
  -webkit-animation: none;
          animation: none;
  border: 0;
  color: #C55;
  display: block;
  z-index: 1;
}

PostCSS Sort Media Queries

分散されたメディアクエリをまとめるために postcss-sort-media-queries をインストールします。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.0.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-declaration-sorter": "^6.0.2",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "file-loader": "^6.2.0",
    "mini-css-extract-plugin": "^1.3.0",
    "postcss-loader": "^4.0.4",
    "postcss-sort-media-queries": "^2.1.11",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.js の postcss-loader に設定を追加します。sort に指定できる値は以下になります。

  • mobile-first:モバイルファーストでまとめる(デフォルト)
  • desktop-first:デスクトップファーストでまとめる
  • 独自の関数を指定
webpack.config.js 抜粋
{
  loader: "postcss-loader",
  options: {
    sourceMap: enabledSourceMap,
    postcssOptions: {
      plugins: [
        'autoprefixer',
        ['css-declaration-sorter', { order: 'alphabetical' }],
        //PostCSS Sort Media Queries(mobile-first でソート)
        ['postcss-sort-media-queries', { sort: 'mobile-firstl' }],
      ],
    },
  },
},
webpack.config.js
//path モジュールの読み込み
const path = require('path');
//MiniCssExtractPlugin の読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// CleanWebpackPlugin の読み込み
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  //エントリポイント(デフォルトと同じなので省略可)
  entry: './src/index.js',  
  //出力先(デフォルトと同じなので省略可)
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    // publicPath を追加
    publicPath: '',
  },
  module: {
    rules: [
      {
        // 対象となるファイルの拡張子(scss)
        test: /\.scss$/,
        // Sassファイルの読み込みとコンパイル
        use: [
          // CSSファイルを抽出するように MiniCssExtractPlugin のローダーを指定
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSSをバンドルするためのローダー
          {
            loader: "css-loader",
            options: {
              //以下を削除
              //url: false,
              // ソースマップを有効に
              sourceMap: enabledSourceMap,
              // css-loader の前に適用されるローダーの数(postcss-loader と sass-loader)
              importLoaders: 2,
            },
          },
          // PostCSS
          {
            loader: "postcss-loader",
            options: {
              // PostCSS でもソースマップを有効に
              sourceMap: enabledSourceMap,
              postcssOptions: {
                // ベンダープレフィックスを自動付与
                plugins: [
                  'autoprefixer',
                  //CSS Declaration Sorter(アルファベット順でソート)
                  ['css-declaration-sorter', { order: 'alphabetical' }],
                  //PostCSS Sort Media Queries(mobile-first でソート)
                  ['postcss-sort-media-queries', { sort: 'mobile-firstl' }],
                ],
              },
            },
          },
          // Sass を CSS へ変換するローダー
          {
            loader: "sass-loader",
            options: {
              // dart-sass を優先
              implementation: require('sass'),
              sassOptions: {
                // fibers を使わない場合は以下で false を指定
                fiber: require('fibers'),
                //outputStyle: 'expanded',
              },
              // ソースマップを有効に
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            //画像を出力フォルダーにコピーするローダー
            loader: 'file-loader',
            options: {
              // 画像ファイルの名前とパスの設定
              name: './images/[name].[ext]'
            }
          }
        ],
      },
    ],
  },
  //プラグインの設定
  plugins: [
    //
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,
    }),
    new MiniCssExtractPlugin({
      // 抽出する CSS のファイル名
      filename: "style.css",
    }),
  ],
  //source-map タイプのソースマップを出力
  devtool: "source-map",
  // node_modules を監視対象から除外
  watchOptions: {
    ignored: /node_modules/  //正規表現で指定
  },
};

例えば、以下のような CSS を style.scss に記述してコンパイルすると、

@media screen and (max-width: 640px) {
  .head { 
    color: #cfcfcf 
  }
}
@media screen and (max-width: 768px) {
  .footer { 
    color: #cfcfcf 
  }
}
@media screen and (max-width: 640px) {
  .main { 
    color: #cfcfcf 
  }
}
@media screen and (min-width: 1280px) {
  .mobile-first { 
    color: #cfcfcf 
  }
}
@media screen and (min-width: 640px) {
  .mobile-first { 
    color: #cfcfcf 
  }
}
@media screen and (max-width: 640px) {
  .footer { 
    color: #cfcfcf 
  }
}

mobile-first の場合、以下のようにまとめられます。

}
@media screen and (min-width: 640px) {
  .mobile-first {
    color: #cfcfcf;
  }
}
@media screen and (min-width: 1280px) {
  .mobile-first {
    color: #cfcfcf;
  }
}
@media screen and (max-width: 768px) {
  .footer {
    color: #cfcfcf;
  }
}
@media screen and (max-width: 640px) {
  .head {
    color: #cfcfcf;
  }
  .main {
    color: #cfcfcf;
  }
  .footer {
    color: #cfcfcf;
  }
}

webpack-dev-server

watch オプション(--watch)を利用すればファイルが変更されると自動的に webpack コマンドを実行するようにはできますがブラウザには反映されません。webpack-dev-server を使えばファイルの変更が自動的にブラウザに反映されるようにできます。

以下は webpack-dev-server を使って開発用サーバを立ち上げる例です。

webpack-dev-server をインストールします。

npm scripts を追加

コマンドラインで簡単にサーバを起動できるように package.json の scripts フィールドに npm scripts を設定します(以下6行目)

package.json 抜粋
"scripts": {
  "build": "NODE_ENV=production webpack --mode production",
  "dev": "webpack --mode development",
  "watch": "webpack --mode development --watch",
  "sass-ver": "sass --version",
  "start": "webpack serve --mode development"
},

webpack 4 では、webpack-dev-server の起動は npx webpack-dev-server でしたが、この時点(2020年11月10日)では webpack-dev-server を実行するとエラーになるので、代わりに npx webpack serve または npx webpack-cli serve で起動します。

関連項目:webpack の基本的な使い方

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "sass-ver": "sass --version",
    "start": "webpack serve --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.0.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-declaration-sorter": "^6.0.2",
    "css-loader": "^5.0.1",
    "fibers": "^5.0.0",
    "file-loader": "^6.2.0",
    "mini-css-extract-plugin": "^1.3.0",
    "postcss-loader": "^4.0.4",
    "postcss-sort-media-queries": "^2.1.11",
    "sass": "^1.29.0",
    "sass-loader": "^10.0.5",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  }
}

これでコマンドラインで npm start または npm run start で開発サーバを起動することができます。

オプションの設定

webpack.config.js に以下の webpack-dev-server のオプションを追加します。

この例の場合、公開するリソース(html ファイル)のルートディレクトリ(ドキュメントルート)は webpack.config.js と同じ位置(デフォルトと同じ)なので省略可能ですが contentBase は path.join(__dirname, '') としています。

また、デフォルトでは webpack-dev-server で生成された(バンドルされた)ファイルはメモリ上に保存されて実際には出力されないので、writeToDisk: true を指定してバンドルされたファイルを出力する(実際に書き出す)ようにしています。

webpack.config.js 抜粋
devServer: {
  //ルートディレクトリの指定
  contentBase: path.join(__dirname, ''),
  //サーバー起動時にブラウザを自動的に起動
  open: true,
  // ルートディレクトリのファイルを監視(変更があると自動的にリロードされる)
  watchContentBase: true,
  //バンドルされたファイルを出力する(実際に書き出す)
  writeToDisk: true
},
webpack.config.js
//path モジュールの読み込み
const path = require('path');
//MiniCssExtractPlugin の読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// CleanWebpackPlugin の読み込み
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// production モード以外の場合、変数 enabledSourceMap は true
const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = {

  //エントリポイント(デフォルトと同じなので省略可)
  entry: './src/index.js',  
  //出力先(デフォルトと同じなので省略可)
  output: { 
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    // publicPath を追加
    publicPath: '',
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, ''),
    //サーバー起動時にブラウザを自動的に起動
    open: true,
    // ルートディレクトリのファイルを監視
    watchContentBase: true,
    //バンドルされたファイルを出力する(実際に書き出す)
    writeToDisk: true
  },
  module: {
    rules: [
      {
        // 対象となるファイルの拡張子(scss)
        test: /\.scss$/,
        // Sassファイルの読み込みとコンパイル
        use: [
          // CSSファイルを抽出するように MiniCssExtractPlugin のローダーを指定
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSSをバンドルするためのローダー
          {
            loader: "css-loader",
            options: {
              //以下を削除
              //url: false,
              // ソースマップを有効に
              sourceMap: enabledSourceMap,
              // css-loader の前に適用されるローダーの数(postcss-loader と sass-loader)
              importLoaders: 2,
            },
          },
          // PostCSS
          {
            loader: "postcss-loader",
            options: {
              // PostCSS でもソースマップを有効に
              sourceMap: enabledSourceMap,
              postcssOptions: {
                // ベンダープレフィックスを自動付与
                plugins: [
                  'autoprefixer',
                  //CSS Declaration Sorter(アルファベット順でソート)
                  ['css-declaration-sorter', { order: 'alphabetical' }],
                  //PostCSS Sort Media Queries(mobile-first でソート)
                  ['postcss-sort-media-queries', { sort: 'mobile-firstl' }],
                ],
              },
            },
          },
          // Sass を CSS へ変換するローダー
          {
            loader: "sass-loader",
            options: {
              // dart-sass を優先
              implementation: require('sass'),
              sassOptions: {
                // fibers を使わない場合は以下で false を指定
                fiber: require('fibers'),
                //outputStyle: 'expanded',
              },
              // ソースマップを有効に
              sourceMap: enabledSourceMap,
            },
          },
        ],
      },
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            //画像を出力フォルダーにコピーするローダー
            loader: 'file-loader',
            options: {
              // 画像ファイルの名前とパスの設定
              name: './images/[name].[ext]'
            }
          }
        ],
      },
    ],
  },
  //プラグインの設定
  plugins: [
    //
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,
    }),
    new MiniCssExtractPlugin({
      // 抽出する CSS のファイル名
      filename: "style.css",
    }),
  ],
  //source-map タイプのソースマップを出力
  devtool: "source-map",
  // node_modules を監視対象から除外
  watchOptions: {
    ignored: /node_modules/  //正規表現で指定
  },
};

main.js の読み込み

今までの例では Javascript をバンドルしていないので html ファイル(index.html) では main.js の読み込みをコメントアウトしていましたが、コメントアウトを外して読み込むようにします。

index.html の例
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="dist/style.css">
<title>Sass 読み込みサンプル</title>
</head>

<body>
<div class="sass_sample">
  <h1>Sass</h1>
  <p>sample text.</p>
</div>
<script src="dist/main.js"></script><!-- main.js を読み込む -->
</body>
</html>

webpack-dev-server を起動すると、main.js に以下のような内容が書き込まれて出力されます(main.js を読み込んでいないとファイルに変更があった場合にライブリロードされません)。

開発サーバの起動

package.json の scripts フィールドに npm scripts を指定してあるので、コマンドラインで npm start を実行すると開発サーバが起動します。

または、以下のように npx コマンドを実行しても同じです(--mode development は開発モード用のオプションで、省略可能です)。

$ npx webpack serve --mode development  return 

この例ではオプションでポート番号を指定していないので、デフォルトの http://localhost:8080/ でページが開きます。

また、オプションで watchContentBase: true を指定しているので、index.html や style.scss を変更するとブラウザがリロードされ変更が自動的に反映されます。

サーバの終了

開発サーバを終了するには control + c を押します。

Dart Sass

webpack を使って Sass をコンパイルすることに直接関係はありませんが、LibSass が非推奨になった(LibSass is Deprecated 2020/10/26 の Sass の Blog )ようなので、関連事項に関する 2020/11/12 時点でのメモです。

@import の廃止

Sass の @import ルールは廃止されることが予定(2022年10月1日)されていて、@use や @forward といったルールに置き換わるようです。置き換わる新しいモジュールシステムは現時点(?)では Dart Sass でしか使えないようです。

参考サイト:

Sass の関連ページ:

Bootstrap 5 関連