🔖

モノレポUI Lib開発におけるCSSモジュールの活用

2024/10/28に公開

背景

  • UIライブラリの開発で複数のUIコンポーネントから利用される共通パーツを定義する
  • UIコンポーネントや共通パーツはそれぞれ独自のスタイルを持つことがある。
  • UIコンポーネントは独立してビルド、パブリッシュができる

目的

  • 各UIコンポーネントのビルド時に、共通コンポーネントのCSSを特定の命名規則に基づいてUIコンポーネント用のスタイルとしてビルドし、共通コンポーネントに対するCSSが複数UIコンポーネント併用時に干渉しないようにする。
  • Elementに対するスタイルの適用をjsで制御する際に、開発者がスタイリング部分の実装をするとき適用可能なスタイルについて型定義を参照できるようにする

レポジトリイメージ

プロジェクト

.
├── build_all.sh
├── inputControls
│   └── textField
│       ├── README.md
│       ├── dist
│       ├── node_modules
│       ├── package-lock.json
│       ├── package.json
│       ├── src
│       ├── tsconfig.json
│       └── vite.config.ts
├── mediaElements
│   └── video
├── shared
│   ├── README.md
│   ├── components
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── tsconfig.json
│   └── utils

UIコンポーネント

textField/vite.config.ts
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import * as path from 'path'


export default defineConfig({
    build: {
        lib: {
            entry: 'src/index.ts',
            name: 'TextField',
            fileName: (format) => `index.${format}.js`,
            formats: ['es', 'umd'],
        },
    },
    css: {
        modules: {
            localsConvention: 'camelCaseOnly',
            generateScopedName: (name) => { //プラグインごとの命名規則
                const prefix = 'ex-text-field';
                return `${prefix}-${name}`;
            }
        }
    },
    plugins: [dts(), cssInjectedByJsPlugin()],// cssのバンドル
    server: {
        open: true,
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src'),
            '@shared': path.resolve(__dirname, '../../shared'), //共通コンポーネントのインクルード
        }
    }
});

UIコンポーネントのビルドフローイメージ

共通コンポーネントのCSS型定義生成

npm i typed-css-modules

css module file イメージ

shared/components/box/styles.module.css
/* Input */
.input {
    padding: 0.5em;
    border: 1px solid #ccc;
    border-radius: 6px;
    box-sizing: border-box;
    background-color: inherit;
    color: inherit;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

package.json スクリプトイメージ

shared/package.json
{
  "scripts": {
    "css-types": "tcm -p 'components/**/*.module.css' --camelCase"
  },
}

Run

npm run css-types

Result

declare const styles: {
  readonly "input": string;
};
export = styles;

まとめ

上記のような構造で共通コンポーネント開発時にはcss moduleに定義したクラスを`import styles from 'styles.module.css'の形式でインポートして型定義つきでts内部で利用しつつ、UIコンポーネントのビルド時には、そのコンポーネントの名前でビルドできる。

開発上は共通コンポーネントを定義しつつ、パブリッシュはUIコンポーネント単位で行う場合には上記で良いが、アセットリストとして複数のUIコンポーネントを含む一つのライブラリとするのであればUIコンポーネントごとに独自のCSSクラスとして定義し直すビルドは不要。

Discussion