はじめに
Progateの舘野です。さまざまなプラットフォーム向けにプロダクトを提供していたり、プロダクトのUIの一貫性を担保するのに何かしらの仕組みの必要性を感じる規模のものを開発していたりすると、デザイントークンのような取り組みが必要になると思いますが、なるべく人を介在せずに運用するにはどうすると良いのでしょうか。
試しにプロトタイプのようなものを作ってみながら考えてみようと思います。
考慮したい項目
まず、運用フローを考える上で考慮しておきたい点を確認しておきます。 細かい点をあげていくとさまざまなものがありそうですが、最低限以下の点をクリアした状態にしたいと思います。
- デザインツールに完全に依存する形はNG
- JSONでSingle Source of Truthとして一元管理されている
- 可視化されたデザイントークンをブラウザから確認できる
デザインツールに完全に依存する形はNG
デザインツールから各プラットフォームのフォーマットへの変換まで行うFigmaのプラグインなどもありますが、デザインツールとしてFigmaやそのプラグインが将来に渡って安定して利用できるかどうかであったり別のデザインツールが台頭するかは不確実だと思うので、あまりデザインツールに強く依存しない方が良いと考えてます。
JSONでSingle Source of Truthとして一元管理されている
プロダクトのUIの一貫性を担保する上で、信頼できる唯一の情報源(Single Source of Truth)として扱うことが重要だと思うので、1箇所に集約して管理することが望ましいかなと思います。
W3C Community Groupのファイルフォーマットに関する言及でもありますが、さまざまな言語でのサポートや普及具合、テキストベースであることで扱いやすいことなどからツール間でのデザイントークンの変換が容易になるので、JSONで定義したいと思います。
可視化されたデザイントークンをブラウザから確認できる
JSONはテキストデータなので、視覚的に内容を把握しやすいとは言えないと思います。なので、プロダクトの開発に関わる誰もが把握しやすいように可視化されている状態にすることが望ましいでしょう。一覧性の高い状態でブラウザ上で確認できると良さそうです。
全体の流れ
上記の項目を踏まえて、全体の流れとしては以下のような形が良いかなと思います。
- Figma Tokens経由でデザイントークンのJSONをGithubリポジトリにPushしてPull Request作成
- Pull Requestをmerge
- Github ActionsでGithub PagesにStorybookをデプロイ
- Github ActionsでStyle Dictionaryを用いて各プラットフォーム向けのフォーマットにデザイントークンを変換してnpmパッケージをpublish
図にするとこのような流れになります。
デザイントークンのソースコードを管理するリポジトリとFigmaのプラグインは連携するけども、デザインツールやそのプラグインが変わっても影響をあまり受けないように、あくまで信頼できる唯一の情報源としてのデザイントークンはJSONファイルとしてデザインツールに依存しない形でGitリポジトリでの管理になります。
Figma Tokensは、Figma上からGithubにデザイントークンをPushもPullも可能なようなので、(自分自身が日常的にFigmaを使っているデザイナーではないので想像に過ぎませんが)おそらくソースコードとの同期が楽になるのではないかと考えています。
デザイントークンを各フォーマットへの変換、npmパッケージとして公開、Storybookで可視化等、Github Actionsでnpm scriptsを実行することでほとんどのことは完結するようにします。
プラットフォーム次第ではnpm以外のパッケージも必要かもしれませんが、今回はnpmに限定して考えます。
利用するライブラリなどについて
主に利用するライブラリやサービスとその用途は以下の通りです。
- Github
- Github Actions: npmパッケージの公開やStorybookのデプロイなどを実行するCI環境として利用
- Github Packages: npmパッケージのレジストリに利用
- Github Pages: Storybookをホスティングするのに利用
- npm
- Style Dictionary: デザイントークンを各プラットフォーム向けにフォーマット変換するのに利用
- semantic-release: npmのリリース作業を任せるのに利用
- Storybook: デザイントークンを可視化するのに利用
- Figma
- Figma Tokens: デザインツールとソースコードとの同期を図るのに利用
試してみる
構成についてはある程度考えられたと思うので、実際に作ってみます。
プロジェクトの作成
プロトタイプとなるプロジェクトを作成します。
このプロジェクトにはStorybookを導入しますが、storybookの初期化コマンドであるsb init
は作成済のプロジェクトに対して利用することに特化しているので、まずはcreate-react-appで適当なプロジェクトを作成します。
$ npx create-react-app design-tokens-package-playground --template typescript
作成したプロジェクトのディレクトリのルートへ移動後にsb init
を実行してStorybookをセットアップします。
$ cd design-tokens-package-playground
$ npx sb init
デザイントークンとなるJSONファイルを用意
Figma Tokensからexport
する機能もありますが、やり方が良くないのか正常に動作しませんでした。今回はひとまずFigma Tokensと互換性のあるJSONのフォーマットを自分で用意します。
サンプルとしてcolor
のred
のみmuiから拝借してJSONに定義し、tokens.json
としてプロジェクトのルートに配置します。
global
というのはFigma Tokensの最上位のキーとなるToken Sets(Theme)です。これが必須なのかどうなのかはあまりよく分かっていませんが、Figma TokensからGithubにPushする際に自動で付与されるような挙動をしているように見えます(正確なことは分かっていません)。
{ "global": { "base": { "red": { "50": { "value": "#ffebee", "type": "color" }, "100": { "value": "#ffcdd2", "type": "color" }, "200": { "value": "#ef9a9a", "type": "color" }, "300": { "value": "#e57373", "type": "color" }, "400": { "value": "#ef5350", "type": "color" }, "500": { "value": "#f44336", "type": "color" }, "600": { "value": "#e53935", "type": "color" }, "700": { "value": "#d32f2f", "type": "color" }, "800": { "value": "#c62828", "type": "color" }, "900": { "value": "#b71c1c", "type": "color" } } } } }
Style Dictionaryで各フォーマットに変換するスクリプトを追加
デザイントークンのJSONファイルが用意できたら、デザイントークンを利用したいプラットフォーム向けに適切なフォーマットにStyle Dictionaryで変換するnpm scriptsを追加します。
最初にnpmを追加します。
$ npm i -E -D style-dictionary
プロジェクトのルートディレクトリにbuild-tokens.js
を用意して、style-dictionary
でデザイントークンを変換するスクリプトを記述します。
const StyleDictionary = require('style-dictionary').extend({ source: ['./tokens.json'], platforms: { json: { buildPath: 'dist/', transforms: ['attribute/cti'], files: [ { format: 'json', destination: 'tokens.json', }, { format: 'custom/json-as-const', destination: 'tokens.json.d.ts', }, ], }, css: { buildPath: 'dist/', transformGroup: 'css', files: [ { format: 'css/variables', destination: 'tokens.css', options: { outputReferences: true, }, }, ], }, }, }) StyleDictionary.registerFormat({ name: 'custom/json-as-const', formatter: ({ dictionary, platform, options, file }) => { return `declare const tokens: ${JSON.stringify( dictionary.tokens, null, 2 )}; export default tokens;` }, }) StyleDictionary.buildAllPlatforms()
extend
で対象のソースファイル(先ほど用意したデザイントークンのJSON)と変換したいフォーマットを指定して、buildAllPlatforms
で変換処理を実行します。
今回はデザイントークンの運用自動化について検証するのが目的なので、さまざまなプラットフォーム向けのフォーマットへ変換することはせずに、JSONとCSSのフォーマットへのみ変換します。
transforms: ['attribute/cti'],
は、CTIを出力結果に付与するものになります。
JSONフォーマットへの変換に関しては、そのJSONをTypeScriptでimportする状況を考慮して型定義ファイルをセットで出力します。
registerFormat
で'custom/json-as-const
として独自定義のフォーマットを登録してtokens.json.d.ts
への変換に利用していますが、これはTypeScriptが現状JSONファイルをimportする際にas const
でreadonlyなオブジェクトの型にできないという点を踏まえて、d.tsを追加したものです。
package.json
のscriptsにtoken:build
として追加します。
"token:build": "node build-tokens.js",
実行してみるとdist
ディレクトリに指定のフォーマットに変換されたものが出力されていることを確認できます。
$ npm run token:build ... > node build-tokens.js json ✔︎ dist/tokens.json ✔︎ dist/tokens.json.d.ts css ✔︎ dist/tokens.css
また、dist
ディレクリはgit管理からは除外したいけどnpmパッケージには含めなければならないので、.gitignore
で除外しつつ.npmignore
では!dist
で除外されないようにしておきます。
プロジェクトルートのtokens.json
は.npmignore
で除外しておいた方が良いかもしれません。
Storybookでデザイントークンを可視化する
次にStorybookでデザイントークンを参照できるようにします。
sb init
で生成されたsrc/stories
ディレクトリにMDXでドキュメント用のファイルを追加します。デザイントークンはコンポーネントではないのでドキュメントだけのMDXとなりますが、その場合<Meta />
のみを利用したMDXとなります。
<Meta title="Tokens/Colors" /> ...
あとは適当にデザイントークンの情報がStorybook上に表示されるようにするだけです。
今回はテーブルで表示するようにだけしておきます。
デザインシステムのようなより大きい枠組みであればコンポーネントをStorybook上に可視化する必要性も出てくると思いますが、デザイントークンだけを可視化したいのであればStorybook以外のもっと軽量なドキュメンテーションツールで代替した方が良いかもしれません。
npmのリリースフローをsemantic-releaseで行うようにする
npmをリリースする作業はsemantic-releaseに全て任せようと思います。
semantic-releaseをインストールします。@semantic-release/changelog
(CHANGELOG生成に利用する)、@semantic-release/git
(git commitに利用する)も一緒にインストールします。
$ npm i -E -D semantic-release @semantic-release/changelog @semantic-release/git
package.json
にrelease
フィールドを追加して、branches
(どのブランチで実行するかを指定)とplugins
(利用するプラグインを指定。なお、個別でインストールしていないプラグインはsemantic-release本体に含まれているもの)を指定します。
"release": { "branches": [ "main" ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/npm", "@semantic-release/github", "@semantic-release/git" ] },
加えて、npmレジストリとしてGithub Packagesを利用することとリポジトリの情報をpackage.json
にそれぞれpublishConfig
とrepository
フィールドで追加します。
"publishConfig": { "registry": "https://npm.pkg.github.com" }, "repository": { "type": "git", "url": "https://github.com/USER_NAME/design-tokens-package-playground.git" },
scripts
にrelease
として追加します。
"release": "semantic-release"
--dry-run
をつけて実行することで、実際にリリースはせずとも挙動を確認することができます。
Github Actionsのワークフローを追加
npmパッケージのリリースに必要なものは概ね揃ったので、Github Actionsのワークフローのファイルを.github/workflows/
配下に追加します。
ワークフローは以下3つ用意します。
check-before-merge.yml
: Pull Requestをmain
ブランチにmergeしても問題ないかをチェック。このプロジェクトではひとまずnpm run token:build
が正常に実行できるかどうかだけをチェックする。deploy.yml
: StorybookをビルドしてGithub Pagesにデプロイする。release.yml
: npmパッケージをGithub Packagesにpublishする。
まずcheck-before-merge.yml
を用意します。main
ブランチへのPull Requestの場合にnpm run token:build
の実行をするようにします。
name: Check before merging on: pull_request: branches: - main jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm ci - run: npm run token:build
deploy.yml
にはStorybookのビルドとGithub Pagesへのデプロイを行うように記述します。
name: Deploy to github pages on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm ci - run: npm run token:build - run: npm run build-storybook - name: deploy uses: peaceiris/actions-gh-pages@v3 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./storybook-build user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com'
mainブランチにpushされたらpeaceiris/actions-gh-pages@v3
を利用してStorybookをGithub Pagesへデプロイします。GITHUB_TOKEN
はワークフロー起動時に自動生成されるので、特にトークン生成の作業は必要ありません。
package.json
のbuild-storybook
のビルドファイルの出力先の指定を上記のpublish_dir
に合わせるように-o
で指定します。
"build-storybook": "build-storybook -o ./storybook-build",
最後にrelease.yml
をnpmのリリースワークフローとして作成します。
name: Publish to github packages on: push: branches: - main jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: git config run: | git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - uses: actions/setup-node@v2 with: node-version: 16 registry-url: https://npm.pkg.github.com - run: npm ci - run: npm run token:build - run: npm run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
registry-url
をGithub Packagesの https://npm.pkg.github.com
に向けておきます。
Githubで確認する
ここまで用意ができたら、あとはGithubのリモートリポジトリにあげてPull RequestをマージしてみるとGithub Actionsのワークフローが動いてnpmパッケージのリリースやStorybookのデプロイが成功していることが確認できます。
gh-pages
ブランチをリポジトリの/settings/pages
でのSource
で指定しておくとStorybookがhttps://username.github.io/reponame/
で公開されます。
npm install
npmパッケージをリリースできたので、そのインストールも試します。
インストール方法はいくつかありますが、Figmaとの連携でも必要になるのでGithubでPersonal Access Tokenを使うのが今回は良いかもしれません。
ローカルでは.npmrc
を用意してPersonal Access Tokenを設定してnpm installするとGithub Packagesからインストールできます。
//npm.pkg.github.com/:_authToken=${PERSONAL_ACCESS_TOKEN} @${OWNER}:registry=https://npm.pkg.github.com/
ただ、Github Actions以外のCI環境などでnpm installするケースでは違うアプローチで認証を通す必要性があるので、npmパッケージ自体はnpmjs.comのレジストリに置く方が諸々の都合が良いかもしれません。
また、現状だとdist
ディレクトリからimport
ということになるので、実際にこのようなnpmパッケージをプロダクトで利用していくのであれば、files
やmain
フィールド、.npmignore
をもう少し整備しないといけないというのもあるでしょう。
import token from '@owner/design-tokens-package-playground/dist/tokens.json
ただ、npmパッケージをインストールして利用することで、UIに関するプリミティブな値を参照するところを揃えられるので、一貫性のあるUIを実現しやすくはなりそうです。
Renovateのようなツールで依存ライブラリの更新を行っている場合には、デザイントークンのnpmパッケージもその対象の1つとなって、更新が容易になるかもしれません。
Figmaとの連携
Figma TokensをGithubのリポジトリを連携させることで、デザイントークンのJSONをリモートのリポジトリからPullすることやFigma上で更新したデザイントークンをリモートリポジトリにPushしてPull Requestを作成することも可能になります。
Sync
のGithub
からAdd new credentials
で必要な情報を入力して登録すれば連携は完了します。連携にはGithubのPersonal Access Tokenが必要になります。
Pull from Github
のボタンを押すだけでからデザイントークンを取得可能になり、Push to Github
のボタンからデザイントークンのリモートリポジトリにPushが可能です。
まとめ
どのような形でのデザイントークンの運用が望ましいか、少し手を動かしながら試してみました。
検証しきれていない部分もありますが、デザインツールに依存せずに、JSONデータのデザイントークンを信頼できる唯一の情報源としてブラウザ上に可視化できることは確認できました。
npmのレジストリがGithub Packagesの場合の認証をどうするかであったり、npmパッケージ自体に含めるファイル群の整理だったり、npm以外でのパッケージ配信についてであったり、デザイントークンの運用のあり方については引き続き考えてみたいと思います。