🐾

モノレポでPandaCSSを利用する3つの方法

2024/06/10に公開3

PandaCSSをモノレポで利用する最適な方法は、プロジェクトの構成によって分化します。
本稿では3種類のパターンを紹介し、また、最も一般的と思われるパターンをサンプルコードを用いて解説します。

また、下記公式ドキュメントと、パターンをまとめられたGithubリポジトリをご参照ください。

https://panda-css.com/docs/guides/component-library

https://github.com/astahmer/panda-monorepo-setup

※ 本稿の内容が最新情報と異なる場合があります。PandaCSSはとくに開発が早いライブラリですので、参考にする際はCHANGELOGを一度ご確認ください。

パターン早見

紹介するパターンは以下です。

  • プリセットのみ共有する
  • 静的CSSファイルを共有する
  • プリセットとstyled-systemを共有する

特にこだわりない方は以下読み飛ばして プリセットとstyled-systemを共有する をご覧ください。

どのような手法が適切かは、プロジェクトの共通コンポーネントパッケージの有無と、アプリ側でPandaCSSを使うかによって決まります。ここでは、turborepoのbasicテンプレートで作成されるプロジェクトを例に考えます。

.
├── apps
│   ├── docs # ドキュメントサイト
│   └── web # アプリケーション本体
├── packages
│   ├── eslint-config
│   ├── typescript-config
│   └── ui # プロジェクトで共有するコンポーネント
└── package.json

どのパターンを採用すべきかざっくり示します。

  • uiでPandaCSSを使う?
    • NO → ✅ プリセットを共有する
    • YES → web, docsでPandaCSSを使う?
      • NO → ✅ 静的CSSファイルを共有する
      • YES → ✅ プリセットとstyled-systemを共有する

web(もしくはdocs)でしかPandaCSSを使わないのであれば、本稿で解説するモノレポ対応の必要はありませんので、Getting Startedに従ってworkspaceに個別に導入します。

Q. それぞれにPandaいれるだけじゃだめなの?

最終的なcssの整合性をとるために、適切な参照とファイル監視を行う必要があります。

PandaCSSのスタイリングには、開発時に生成されたstyled-systemcssが必要です。

styled-systemにはスタイリングに必要なcss()やトークンの型などが含まれます。パッケージ間で異なるstyled-systemを使うと、デザイントークンに違いが生じたり、レシピ・パターンの共有が行えません。

cssは言わずもがな、PandaCSSによるスタイリングの出力です。PandaCSSはビルド時にファイルをASTで解析し、PandaCSSの使用箇所からcssをパースします。この出力におけるソースが複数に分かれる場合をモノレポでは考慮する必要があります。

プリセットを共有する

最も簡単な方法です。
複数のパッケージでPandaCSSを使いたい、それらのパッケージ間でコンポーネントの共有を行う必要がない場合に有用なパターンになります。

初期化時に生成されるpanda.config.tsに適用する値は、PandaCSSのユーティリティを用いて定義できます。ただのオブジェクトですので、package.jsonでexportsすればビルドせずに利用可能です。

具体的な方法は公式ドキュメントのship-a-panda-presetをご覧ください。
https://panda-css.com/docs/guides/component-library#ship-a-panda-preset

なお、この方法では各パッケージにstyled-systemが生成されます。スタイリングの際はそのstyled-systemを参照してください。

静的CSSファイルを共有する

uiのビルド時にcssファイルを一度ビルドし、uiを利用しているパッケージで読み込みます。
uiパッケージのみがPandaCSSに依存するため、他のパッケージでPandaCSSの使用が強制されません。

具体的な方法はこちらも公式ドキュメントをご覧ください。
https://panda-css.com/docs/guides/component-library#ship-a-static-css-file

web,docsでPandaCSSを使う場合も、プリセットの共有と組み合わせて使うことでスタイルの適用は一応可能です。
その場合、uiでビルドしたcssと、webでビルドしたcssが重複する可能性があるため、configのprefixを設定して回避します。重複した分バンドルサイズが増えることも留意してください。

プリセットとstyled-systemを共有する

大体のプロジェクトにおいて最適と思われる構成がこちらになります。
プリセットとstyled-systemをプロジェクトで共有することで最終的に生成されるcssを一致させます。

この構成のサンプルリポジトリは以下になります。
https://github.com/HNitta0605/panda-monorepo-example

最終的なディレクトリ構成は以下になります(一部割愛)。

.
├── README.md
├── apps
│   ├── docs
│   │   ├── app
│   │   ├── public
│   │   ├── next.config.js
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── web
│       ├── app
│       ├── public
│       ├── next.config.js
│       ├── package.json
│       ├── panda.config.ts
│       └── postcss.config.cjs
├── packages
│   ├── eslint-config
│   │   ├── library.js
│   │   ├── next.js
│   │   ├── package.json
│   │   └── react-internal.js
│   ├── panda-config
│   │   ├── package.json
│   │   ├── panda.config.ts
│   │   └── tsconfig.json
│   ├── styled-system
│   │   └── package.json
│   ├── typescript-config
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   └── ui
│       ├── src
│       ├── tsconfig.json
│       ├── tsconfig.lint.json
│       ├── package.json
│       └── turbo
├── pnpm-workspace.yaml
├── package.json
├── tsconfig.json
└── turbo.json

定義順に解説します。

/packages/panda-config

PandaCSSのプリセットを定義します。
panda.config.tsにて基本となるconfigを定義し、buildスクリプトでstyled-systemを生成します。

packages/panda-config/panda.config.ts
import { defineConfig } from '@pandacss/dev';

export default defineConfig({
  // outdirでstyled-systemの出力先を指定する
  outdir: '../styled-system',
  preflight: true,
  strictTokens: true,
  strictPropertyValues: true,
  jsxFramework: 'react',
  theme: {},
  globalCss: {},
});

また、panda.config.tsをexportsに追加し、他のパッケージがconfigをimport可能にします。

pacages/panda-config/package.json
{
  "name": "@repo/panda-config",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "build": "panda codegen"
  },
  "devDependencies": {
    "@pandacss/dev": "^0.38.0",
    "@repo/typescript-config": "workspace:*"
  },
  "exports": {
    ".": "./panda.config.ts"
  }
}

/packages/styled-system

@repo/panda-configのbuildコマンドによってビルドされるPandaのソースコードです。
ビルド後、以下ディレクトリが生成されます。

packages/styled-system
├── css
├── tokens
├── jsx
├── types
├── patterns
├── helpers.mjs
└── package.json

このディレクトリのpackage.json以外をgitignoreの対象とします。

.gitignore
## Panda
packages/styled-system/**/*
!packages/styled-system/package.json

注意点として、jsxオプションをtrueにしている場合、@types/react@types/react-domのinstallが必須です。型定義のエラーが発生した場合は確認してください。

/packages/ui

ライブラリとして使う共通コンポーネントパッケージです。
今回はビルドをせず、Typescriptを直接exportsする手法を想定して解説します。
@repo/styled-systemをdependenciesに追加し、思うままにスタイリングします。

packages/ui/src/button
'use client';

import { ReactNode } from 'react';
import { css, cx } from '@repo/styled-system/css';

interface ButtonProps {
  children: ReactNode;
  className?: string;
  appName: string;
}

export const ButtonStyle = css({
  fontSize: 'lg',
  padding: '1',
  borderWidth: 'thin',
  borderColor: 'sky.200',
  borderRadius: 'sm',
  cursor: 'pointer',
  _hover: {
    backgroundColor: 'sky.100',
  },
});

export const Button = ({ children, className, appName }: ButtonProps) => {
  return (
    <button
      type="button"
      className={cx(ButtonStyle, className)}
      onClick={() => alert(`Hello from your ${appName} app!`)}
    >
      {children}
    </button>
  );
};

/apps/web

アプリケーションの本体を想定します。
@repo/panda-config, @repo/styled-system, @repo/uiをdependenciesに追加します。

最終的にcssを生成するパッケージなので、panda.config.tspostcss.cjsを作成します。

apps/web/panda.config.ts
import { defineConfig } from "@pandacss/dev";
import config from "@repo/panda-config";

export default defineConfig({
	...config,
	include: [
		"./app/**/*.{ts,tsx}",
		"./components/**/*.{ts,tsx}",
		"./node_modules/@repo/ui/src/**/*.{ts,tsx}",
	],
	dependencies: ["./node_modules/@repo/ui/src/**/*.{ts,tsx}"],
	importMap: "@repo/styled-system",
});

panda.config.tsのポイントとして、includedependenciesにuiパッケージを含めてください。
includeで、cssを出力する際に対象とするファイルを指定し、dependenciesはスタイルのホットリロードを有効化します。つまるところ、PandaCSSによるスタイリングを行っている箇所すべてを指定します。

pnpmは上記のようなnode_modules内に存在するシンボリックリンクへのパスで問題ありません。

また、global.cssに通常PandaCSSを使う際のCascade Layerの記述も忘れずに行ってください。

apps/web/app/global.css
@layer reset, base, tokens, recipes, utilities;

Next.js側のホットリロードを効かせるにはnext.configのtranspilePackagesオプションにuiを追加します。

apps/web/next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  transpilePackages: ["@repo/ui"],
};

最後に、webの開発環境起動前に@repo/panda-configのbuildを行いたいので、turbo.jsonで依存スクリプトを設定しておきましょう。

apps/web/turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "extends": ["//"],
  "pipeline": {
    "dev": {
      "dependsOn": ["@repo/panda-config#build"]
    },
    "build": {}
  }
}

以上でおおまかな構築は完了です。

おわりに

PandaCSSの良さは、どのようなプロジェクトにも形を変えて適応できる柔軟性にあると思っています。tailwindが良くも悪くもclassに囚われる一方で、PandaCSSはstyledコンポーネントにも対応します。

一方、その柔軟性ゆえにconfigが難しい側面もあります(ここまで解説してきたように)。設定してしまえば型安全なスタイリングという意味で唯一無二のDXになりますので、ぜひチャレンジしてみてください。

Discussion

ハトすけハトすけ

こんにちは!
とても良い記事ありがとうございます、助かりました^^

1つpandacssがうまく対応してくれない部分があり、もし対応策を知っていたら教えてほしいのです。
こちらの記事の通りプリセットとstyled-systemを共有しました。
そして、apps内部やpackages/uiでpandacssのエスケープハッチな書き方をしました。

css({ fontSize: '[123px]' })

すると、うまくpandacssがcss化してくれません。
なにかご存知でしょうか?

inuruninurun

コメントありがとうございます!
返信遅れてすみません!もう遅いかもしれませんが回答いたします。

個人的な経験ですが、PandaCSSでスタイルが適用されない際の原因は大きく3つです。

  1. 定義箇所がPandaCSSによる監視対象に含まれていない
  2. 定義箇所がHMRの監視対象に含まれていない
  3. cssをimportできていない

「ブラウザを更新すると適用される」という状態であれば、2のHMRの監視設定が漏れている可能性があります。Next.jsであればnext.config.jstranspilePackagesの設定をご確認ください。

「escape-hatchを用いない通常のスタイルであっても適用されない」状態であるなら、1か3の設定ミスが考えられます。各パッケージのpanda.config.tsを確認してみてください。

設定に不足ないようであれば、packagesに配置したstyled-systemを参照せず、各パッケージごとにstyled-systemを生成し、そちらのcss関数を参照して解決するかを確認していただければ、問題が少し絞り込まれるかと思います。お試しください。

...ここまで書きましたが、escape-hatchのみ適用されないという状態に陥ったことがなく...
直接的な言及ができず申し訳ありません。よろしくお願いいたします。

ハトすけハトすけ

丁寧な返信ありがとうございます^^
なるほど、そのような可能性があるのですね。一度一通り試してみます!

原因がわかれば、またこちらに共有します。