デザイントークンの運用自動化について考える

はじめに

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はテキストデータなので、視覚的に内容を把握しやすいとは言えないと思います。なので、プロダクトの開発に関わる誰もが把握しやすいように可視化されている状態にすることが望ましいでしょう。一覧性の高い状態でブラウザ上で確認できると良さそうです。

全体の流れ

上記の項目を踏まえて、全体の流れとしては以下のような形が良いかなと思います。

  1. Figma Tokens経由でデザイントークンのJSONをGithubリポジトリにPushしてPull Request作成
  2. Pull Requestをmerge
    1. Github ActionsでGithub PagesにStorybookをデプロイ
    2. Github ActionsでStyle Dictionaryを用いて各プラットフォーム向けのフォーマットにデザイントークンを変換してnpmパッケージをpublish

図にするとこのような流れになります。

f:id:makotot-riceball:20211222181318p:plain
全体の流れ

デザイントークンのソースコードを管理するリポジトリと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のフォーマットを自分で用意します。

サンプルとしてcolorredのみ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上に表示されるようにするだけです。

今回はテーブルで表示するようにだけしておきます。

f:id:makotot-riceball:20211222182027p:plain
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.jsonreleaseフィールドを追加して、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にそれぞれpublishConfigrepositoryフィールドで追加します。

  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/USER_NAME/design-tokens-package-playground.git"
  },

scriptsreleaseとして追加します。

"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.jsonbuild-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のデプロイが成功していることが確認できます。

f:id:makotot-riceball:20211222182345p:plain
semantic-releaseによるリリース完了のコメント

f:id:makotot-riceball:20211222182511p:plain
deployワークフローでのgh-pagesブランチへのpush

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パッケージをプロダクトで利用していくのであれば、filesmainフィールド、.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を作成することも可能になります。

SyncGithubからAdd new credentialsで必要な情報を入力して登録すれば連携は完了します。連携にはGithubのPersonal Access Tokenが必要になります。

f:id:makotot-riceball:20211222182711p:plain
Figma TokensのGithub連携

Pull from Githubのボタンを押すだけでからデザイントークンを取得可能になり、Push to GithubのボタンからデザイントークンのリモートリポジトリにPushが可能です。

まとめ

どのような形でのデザイントークンの運用が望ましいか、少し手を動かしながら試してみました。

検証しきれていない部分もありますが、デザインツールに依存せずに、JSONデータのデザイントークンを信頼できる唯一の情報源としてブラウザ上に可視化できることは確認できました。

npmのレジストリがGithub Packagesの場合の認証をどうするかであったり、npmパッケージ自体に含めるファイル群の整理だったり、npm以外でのパッケージ配信についてであったり、デザイントークンの運用のあり方については引き続き考えてみたいと思います。