ASP.NET Core Web アプリケーションのプロジェクトテンプレートで CSS Modules を使う

ASP.NET Core Web アプリケーションのプロジェクトテンプレートで CSS Modules を使う
====

結論から言うと、プロジェクト独自に webpack や babel の設定をする必要はない。

react-scripts パッケージの webpack.config.js の設定に従って、ファイル名の末尾を `.module.css` とした CSS ファイルをスクリプトファイルからインポートすれば使用が可能となる。


状況の再現
----

疑問を感じた状況の再現。

本記事は、以降「調査の記録」に終始している。初学者の学習メモ的なものであり、他人が読んでも特に有益な情報はないと思われる。

### プロジェクトの作成

Visual Studio 2019 の プロジェクトテンプレート ASP.NET Core Web アプリケーション「React.js と Redux」を使用して、 SPA プロジェクトを作成した。

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

ClientApp ディレクトリ下で、下記パッケージをインストール。

```shell
$ npm install --save-dev \
      webpack-cli style-loader css-loader typescript \
      babel-loader babel-preset-typescript babel-preset-env babel-preset-react \
      babel-plugin-transform-class-properties
```

### コンフィグファイルの作成

ClientApp ディレクトリ下に下記内容のファイル群を作成。

```js:ClientApp/webpack.config.js
const path = require("path");

module.exports = {
    mode: "development",
    entry: path.resolve("src", "index.tsx"),

    // ファイルの出力設定
    output: {
        path: path.resolve("public"),
        filename: "bundle.js",
    },

    module: {
        rules: [
            // TypeScript
            {
                exclude: ["/node_modules/"],
                test: /\.[jt]s(x?)$/,
                use: ["babel-loader"]
            },

            // CSS
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader?modules"]
            },
        ]
    },
    resolve: {
        extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
    }
};
```

```json:ClientApp/.babelrc.json
{
  "presets": [
    "@babel/preset-typescript",
    "@babel/preset-env",
    "@babel/preset-react",
    {
      "plugins": [
        "transform-class-properties"
      ]
    }
  ]
}
```

### 既存ソースファイルの編集

ClientApp/src ディレクトリ下のソースファイルを編集。

CSS Modules 使用のため同階層に CSS ファイルを追加し、読み込んでいる。

```tsx:ClientApp/src/components/Home.tsx
import * as React from 'react';
import { connect } from 'react-redux';

// 追加行
import styles from './Home.css';
console.log(styles);

const Home = () => (
    <div>
        <h1 className={styles.title}>Hello, world!</h1>
        (省略)
    </div>
);

export default connect()(Home);
```

```css:ClientApp/src/components/Home.css
.title {
    background-color: #ff0000;
}
```

### webpack の実行

ClientApp ディレクトリ下で、下記コマンドを実行。エラーなく動作完了。

```shell
$  ./node_modules/.bin/webpack
```

### サーバーの起動

サーバーを起動。タイトルの背景色は変化していない。

CSS をインポート内容を出力するための `console.log(styles)` でも `{}` と空オブジェクト。読み込まれていない。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42483/1c4fa4a4-d540-23bb-babe-e25c5548a6b1.png)
|:--|



検証
----

そもそもサーバーの起動時、どういう流れで npm が実行されているかを把握していなかった。

手動で webpack を実行できても、サーバー起動時に実行され、そのコンパイル結果がサーバーアプリ側で使われていなければ意味がない。

### Startup.cs の確認

npm の開始処理は下記ファイル `npmScript` 部に記述されていた。

```csharp:Startup.cs
namespace WebApplication20200816
{
    public class Startup
    {
        // 省略

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
            // 省略

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseReactDevelopmentServer(npmScript: "start");
                }
            });
        }
    }
}
```

### csproj の確認

プロジェクトの csproj を確認する。

npm ビルド処理は `Name="PublishRunWebpack"` 部に記述されていた。

```xml:WebApplication20200816.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <!-- 省略 -->

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**; $(SpaRoot)build-ssr\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

</Project>
```

### package.json の確認

ClientApp ディレクトリ下の package.json を確認する。

`npm run` コマンドの実行処理呼び出し部は `scripts` 下に記述されている。見慣れない `react-scripts` というコマンドが記述されている。

```json:package.json
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "cross-env CI=true react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "lint": "eslint ./src/**/*.ts ./src/**/*.tsx"
  }
```

### node_modules/react-scripts の確認

ディレクトリ内のファイルを確認する。

scripts ディレクトリ下にコマンドが存在し、 config ディレクトリに webpack.config.js を発見する。

サーバーの起動時は、前述 package.json の `react-scripts` コマンドでは強制的にこのファイルを使うのかもしれない。

```
LICENSE
package.json
README.md
config\env.js
config\getHttpsConfig.js
config\modules.js
config\paths.js
config\pnpTs.js
config\webpack.config.js
config\webpackDevServer.config.js
config\jest\babelTransform.js
config\jest\cssTransform.js
config\jest\fileTransform.js
lib\react-app.d.ts
scripts\build.js
scripts\eject.js
scripts\init.js
scripts\start.js
scripts\test.js
scripts\utils\createJestConfig.js
scripts\utils\verifyPackageTree.js
scripts\utils\verifyTypeScriptSetup.js
template\README.md
template-typescript\README.md
```

### node_modules/react-scripts/config\webpack.config.js の確認

今回の webpack を使う目的は CSS Modules の使用であるため、そこに限定して記述を確認する。

CSS Modules としてスクリプトから CSS をインポートするには、ファイル名の末尾を `.module.css` とする必要があるようだ。

```js:node_modules/react-scripts/config\webpack.config.js
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
            // 省略

            {
              test: cssRegex,
              exclude: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
            // using the extension .module.css
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
                modules: {
                  getLocalIdent: getCSSModuleLocalIdent,
                },
              }),
            },

            // 省略
```



サーバーアプリの修正
----

### ファイルの編集

CSS Modules のインポート元・インポート先のファイル名を `.module.css` の形式に変更する。

```tsx:ClientApp/src/components/Home.tsx
import * as React from 'react';
import { connect } from 'react-redux';

// 追加行
import styles from './Home.module.css';
console.log(styles);

const Home = () => (
    <div>
        <h1 className={styles.title}>Hello, world!</h1>
        (省略)
    </div>
);

export default connect()(Home);
```

### サーバーの起動

CSS Modules として CSS がインポートされている。

`console.log(styles)` でも CSS Modules 特有のクラス名をもったオブジェクトが出力されている。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42483/6d227211-c474-3c9f-5c5d-800d2b824b9a.png)
|:--|



対応後
----

### プロジェクトの修正

不要になったプロジェクト中の webpack 関連パッケージ、ファイルを削除した。

### 今後の方針

webpack を用いた設定はまず react-scripts パッケージの設定を確認・流用する。

### 独自の webpack 設定を使いたい場合

今回の対応は「独自のものは用意しない」方針となったが、どうしても独自の設定を仕込みたい場合は今後発生する可能性がある。

解決策を探しているうちに、その方法も多少見つかったのでメモしておく。 (詳細は未調査)

#### react-app-rewired を使用する

[react-app-rewired](https://github.com/timarney/react-app-rewired) を使用し、 react-scripts の webpack 設定を上書きする。

#### react-scripts を使用しない

プロジェクトに完全な形のコンフィグファイルを作成する。

今回のようにプロジェクトテンプレートを使用している場合、設定が入り組んでいるため react-scripts の設定を流用するのが安心な気はする。同時に package.json の `scripts` 部を `react-scripts` に依存しない形に修正する。

プロジェクトテンプレートを使用していない場合、最小限の構造から徐々に拡張していく方が混乱なくてよいかと思う。 react-scripts パッケージのコンフィグファイルは設定が充実し過ぎている気がするし、小規模な webpack.config.js の例はウェブ上に多数存在する。

webpack に関するプロジェクトテンプレートの使用状況は、すなわち `create-react-app` の使用状況と同等かもしれない。