🦁

デザインシステムのESLintプラグインから運用のヒントを学ぶ

2024/02/28に公開

はじめに

2021年のデザインシステムに関する調査によると、回答数があまり多くないものの、成功を収めているというデザインシステムは4割程度という結果が出ている。

But a majority of design systems ended up imploding. According to the 2021 Design Systems Survey, only 40% of the systems were successful.

https://storybook.js.org/blog/why-most-design-systems-implode

また、alexpate/awesome-design-systems: 💅🏻 ⚒ A collection of awesome design systemsAdeleといったデザインシステムの事例集のリンク先は、メンテナンスを継続してなかったり滞っているケースが散見される。

このようにデザインシステムは、継続が難しい(技術的な要素よりも組織的な要素が大きいと思うけど)ことが窺えるものだけど、ここではESLintのような静的解析からその継続の一助となるようなものがあるか探ってみたい。

なお、デザインシステムがコンポーネントライブラリーのみを指すわけじゃないけど、ここでは開発に寄った内容を扱うので便宜上デザインシステムやコンポーネントライブラリーなどの定義を細かく分けて扱わない。

デザインシステムのESLintプラグイン

一般的にJavaScriptやTypeScriptを利用したプロジェクトの開発では、ESLintのような静的解析でプラクティスやコーディング規約を機械的に強制したり推奨したりすると思う。
特定の概念に対するESLintのカスタムルールをまとめたESLintプラグインも数多く存在するけど、デザインシステムのESLintプラグインというのも幾つかある。

ここでは、AtlassianGitHubSumup、それぞれのデザインシステムに関するESLintプラグインを見てみる。

@atlaskit/eslint-plugin-design-system

Atlassianには@atlaskit/eslint-plugin-design-systemというESLintプラグインがあり、 "Atlassian Design Systemを利用する際に必要不可欠なプラグイン" として扱われている。

The essential plugin for use with the Atlassian Design System.

https://atlassian.design/components/eslint-plugin-design-system

このプラグインのルールを幾つかピックアップしてみる。

ensure-design-token-usage

デザイントークンの値を参照することを強制するルール。プロダクトにおいてテーマを切り替えた時、部分的にテーマが適用されない不具合を避ける目的のルール。

All experiences must use color tokens otherwise when switching between themes, unexpected incidents can occur where not all of the UI is themed.

no-banned-imports

デザインシステムのプライベートなモジュールや実験的なモジュールをimportしないようにというもの。exportsフィールドで制限できる部分かなと思うけど、exportsフィールドが登場する前からあるルールなのかもしれない。

デザインシステムのアップデートを難しくする密結合を防ぐ意図があると思う。

Using private or experimental packages is dangerous as they are not supported across major versions meaning you will not be able to migrate easily causing friction for yourself and the Atlassian Design System team.

no-css-tagged-template-expression

@emotion/reactstyled-componentsなどのcssタグ付きテンプレートリテラルを禁止するルール。
テンプレートリテラルはオブジェクトリテラルと比較するとAST解析が難しいので、その利用を禁止している。

元々はAtlassianがOSSで開発しているCompiledのESLintプラグインのルールをポートしたもの。

なお、Atlassian Design SystemのESLintプラグインのルール一覧をみると、recommendedには入ってない。

no-deprecated-apis

非推奨となったデザインシステムのPropsの利用を禁止するルール。非推奨となったPropsが残っている間、そのPropsを含めてメンテナンスし続けていくということになるので、切り替えを強制するためにLintで機械的に弾くというもの。

no-deprecated-imports

非推奨となったデザインシステムのパッケージのimportを禁止するルール。no-deprecated-apisと同じように、速やかに非推奨のものを排除していくことが目的だと考えられる。

no-empty-styled-expression

以下のような空っぽの引数のstyled式を禁止するルール。

const EmptyStyledDiv = styled.div();
const EmptyStyledOjbectDiv = styled.div({});

このような場合、CSS-in-JSライブラリーによって不要なビルドプロセスを経ることなく、直接divを用いた方がパフォーマンス観点で望ましいというもの。

no-margin

コンポーネントに自身の外のレイアウトの関心を持たせるな、というのはよく言われることだけど、それをLintルールとして体現しているもの。

https://qiita.com/otsukayuhi/items/d88b5158745f700be534

marginの代わりに親要素へgapを指定することで多くのレイアウトは実現できるはずで、コンポーネント駆動開発におけるコンポーザビリティーを阻害する大きな要因になると思うけど、marginの利用自体をLintで機械的に禁止するというのはなかなか難しいように思える。
Atlassian Design SystemのESLintプラグインのルール一覧においてもno-marginRecommendedじゃないので、ルールの採用は局所的なのかもしれない。

no-nested-styles

ネストしたスタイルを禁止するルール。以下のように子要素を含めてスタイリングしていると、その子要素の変更によって予期せずレイアウト崩れが生じる可能性を持つ。

css({
  div: {
    display: "flex`",
  },
});

まとめてスタイリングせずに、しっかりコンポーネントを分けて細かくコンポーネント毎にスタイリングしましょうというもの。

no-styled-tagged-template-expression

no-css-tagged-template-expressionと類似するもので、styledのタグ付きテンプレートリテラルを禁止するルール。
CSSの文法上の誤りを静的に検知しにくく、型の制約を持ち込みにくく、シンタックスハイライトも充分なサポートを得られないケースがある。そのためオブジェクトリテラルで記述するべきというもの。

no-css-tagged-template-expression同様にこちらもAtlassian Design SystemのESLintプラグインのルール一覧ではrecommendedに入ってない。

no-unsafe-style-overrides

デザインシステムのコンポーネントのスタイルを上書きすることを禁止するルール。ただし、xcsspropを利用する形でのスタイリングは許容している。
xcssはAtlassian Design Systemのデザイントークンとプリミティブコンポーネントを繋ぐAPIで、デザイントークンの値のみをプロパティーとして利用する型安全性が保たれていて、ネストしたセレクターの利用を排除するもの。

  • XCSS has type-safety that ensures token name usage for all CSS properties represented by design tokens.
  • XCSS restricts nested selectors completely from usage

レイアウトプリミティブのコンポーネントに対してのみxcsspropが付与されている形になると思うので、ほとんどのコンポーネントにおいては、そのコンポーネントのスタイル上書きが禁止されることになる。
コンポーネントのスタイル上書きが必要な場合でも、一定の制約の下でスタイリングすることを強制している。
Lintルールにunsafeと入っているように、コンポーネント内部のスタイルへの直接的な干渉は、コンポーネントの変更によって起こり得る予期しないレイアウト崩れ等を鑑みるとリスクが大きいはず。

eslint-plugin-primer-react

GitHubのデザインシステムであるPrimerでは、eslint-plugin-primer-reactがReact実装に対する実験的なプラグインとして開発されている。
実験的というのは、eslint-plugin-primer-react is experimental.と記載があることを指している。
まだ実験的な段階ということもあってか、上述の@atlaskit/eslint-plugin-design-systemに比べるとルール数は多くないものの、ReactにおけるPrimerのベストプラクティスを徹底させる目的のLintルールを持つものになっている。

Primer React offers an ESLint plugin to enforce best practices and fix common problems.

https://primer.style/react/linting

こちらも2つほどルールをピックアップしてみる。

direct-slot-children

Primerにおいては、いくつかのコンポーネントでslotsパターンによりサブコンポーネントを特定の場所へレンダリングしていて、そのサブコンポーネントがメインのコンポーネント直下に置かないといけない構成になっている。
slotsパターンとは、コンポーネントが渡された要素のレンダリング場所をコントロールするパターンのこと。

Slots allow us to pass elements to a component and tell the component where these elements should be rendered. A component may support an implicit default slot (unnamed) and optionally, one or more named slots.

https://sandroroth.com/blog/react-slots

このslotsパターンのメインコンポーネントとサブコンポーネントのコンポーネントツリーの構造をLintで強制するというもの。

a11y-tooltip-interactive-trigger

Tooltipコンポーネントで非インタラクティブ要素をラップしないようにするa11y観点のルール。
状況に応じでどのような実装が望ましいのかを丁寧にLintエラーのメッセージで伝えてくれることがLintルールの実装から分かる。

https://github.com/primer/eslint-plugin-primer-react/blob/bd65052028bf6a6e8e83910461384a280b06f0da/src/rules/a11y-tooltip-interactive-trigger.js#L144-L151

@sumup/eslint-plugin-circuit-ui

SumupというFinTechのデザインシステムのUIライブラリーとしてCircuit UIというものがあり、そのESLintプラグインとして@sumup/eslint-plugin-circuit-uiがある。

ESLint rules to help users follow best practices when using Circuit UI.

https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui

こちらもルール自体は多くないもののベストプラクティスを実践する助けになるプラグインとして用意されている。

このプラグインも3つほどルールをピックアップしてみる。

component-lifecycle-imports

広範に渡って利用されているデザインシステムではコンポーネントライフサイクルを定義しているケースがあると思うけど、Circuit UIもライフサイクルがある。

https://circuit.sumup.com/?path=/docs/introduction-component-lifecycle--docs

ステージによってコンポーネントのサポートレベルは異なり、Legacyステージにあるコンポーネントであればバグフィックスのみのサポートになる。
このようなサポート条件の違いをimportパスの違いによって示していて、それをLintでauto fixできるものになっている。

no-deprecated-components

Deprecatedのステージにある非推奨コンポーネントは次のメジャーバージョンアップデートで削除するものとして扱っていて、段階を経て削除へと至る運用になっている。

  1. マイナーバージョンアップデートでコンポーネントをDeprecated
  2. 次のメジャーバージョンアップデートでコンポーネントを削除

マイグレーションガイドに削除されるコンポーネントに対する対応手段が示されるので、利用している方ではそのガイドに応じた対応を行なえば良く、一般的なOSSのライブラリーのメジャーバージョンアップデート時と同じように考えることができる。

no-renamed-props

一貫性を徹底するためにコンポーネントのpropをリネームすることがあり、古くなったpropの名前を自動で修正するためのルール。
no-deprecated-componentsと同じように段階的な切り替えで運用される。

  1. マイナーバージョンアップデートで新しい名前のprop追加して、古い名前は同時に非推奨扱いへ
  2. 次のメジャーバージョンアップデートで古い名前は削除される

まとめ

それぞれのデザインシステムにおけるコンテキストに応じて多様なルールが存在するけど、プラクティスを機械的に強制してくれるルール以外だとデザインシステムのバージョンアップに追随していく助けとなるルールが興味深いなと思う。
他にもデザインシステムでESLintプラグイン作っているところがあるなら、どういうものがあるか見てみたいけど、公開されているものはあまりないのかもしれない。

参考

GitHubで編集を提案

Discussion