Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

OpenAI API を使ったデザインからコード生成する Figma プラグイン

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyに、6月からお試し入社する seya(@sekikazu01)と申します。

ここしばらく話題になっている、OpenAI が提供する ChatGPT を代表とした LLM。この記事では、そんな OpenAI の API を使って Figma からコード生成するプラグインを作ってみた過程を記していこうと思います。

先に背景をちょっとお伝えしますと、Gaudiy ではPSFに向けて、複数パターンのUI・機能を実際に提供しながら検証を回すことを予定しています。

すでに定義したコンポーネントはある程度使い回せるものの、ページ実装の試行回数の増加が見込まれる状況です。ここの作業効率化のために、コンポーネントをしっかり活用しながらも、ちょっといじればプロダクション利用可能な React コードを Figma から書き出すトライとして開発しています。

現状の肌感としては「デザインがしっかり構造的に作られている」「コンポーネントがデザインとコードで同期されている」状態においては、だいぶ実践投入できるレベルのコードを出してくれる印象です。

ですが、まだまだ課題はたくさんあり、それはこのプラグインの機能のみならず組織のデザインと実装のプロセスにも関わるものです。なので OSS にもしてみたので、ご興味ある方に Contribute いただいたり、働き方の知見の共有をしていけると嬉しいです。

github.com

とは言いつつ現状 GPT-3.5-turbo だとそんなにクオリティの高いコードを出してくれないので、GPT-4 が必要になります。GPT-4 API のアクセスがない方はお待ちいただくか、3.5 でいい結果の出るプロンプトを模索してみてください。

デモ動画

まず、実際どんなものなのかデモ動画を用意したので下記をご覧ください。

youtu.be

※動画ではさも一瞬で生成されたかのように見えますが、実際は1分くらいかかっています。

▼ 忙しい人のための静止画

基本的な作り

はじめに、ざっくりとした生成プロセスをまとめると次のような形です。 Figma のレイヤーを JSON に変換 -> それを OpenAI API に渡してコードを書いていただいてます。

デザインを OpenAI を通じてコード化するフロー図

Figma プラグインでは、Figma のレイヤーやコンポーネントなどの情報を参照することができます。 例えば下図は Frame のレイヤーから取れる情報の型定義です。Auto Layout 関連の情報などが取得できることが見て取れると思います。

Auto Layout の型情報
Auto Layout の型情報

ただ、これはそのままでは余計な情報が多いのと、Figma 専用の語彙になっていることがちらほらあり(primaryAxisSizingMode, counterAxisSizingMode の組み合わせなど)、OpenAI API さんではうまく解釈できないものがあるため、最初にCSSの語彙に直したオブジェクトに変換します。

あまり詳細は書いてもアレなのでざっくり概要のコードだけ貼ると、下記のように単純に Figma の node(レイヤー)の children を辿っていって、必要な情報だけ持ったオブジェクトに変換しています。

また、コンポーネントのインスタンスの場合はコンポーネントとして表示したいので、それ以上 children は深追いせず ComponentNode という個別の型のオブジェクトに変換しています。(https://github.com/kazuyaseki/figma-code-transformer/blob/main/src/figmaNode/buildTagTree.ts)

    node.children.forEach((child) => {
      if (child.type === 'INSTANCE') {
        const props = Object.keys(child.componentProperties).reduce(
          (_props, key) => {
            const value = child.componentProperties[
              key
            ] as ComponentProperties[string];

            // component property keys are named like this: "color#primary"
            // thus we need to split the key to get the actual property name
            const _key = value.type === 'VARIANT' ? key : key.split('#')[0];
            return { ..._props, [_key]: value.value };
          },
          {} as { [property: string]: string | boolean }
        );

        if ('Instance' in props) {
          delete props['Instance'];
        }

        childTags.push({
          name: child.name.replace(' ', ''),
          props,
          isComponent: true,
          children: [],
        });
        if (child.mainComponent) {
          componentNodes.push(child.mainComponent);
        }
      } else {
        const childTag = buildTagTree(child, componentNodes);
        if (childTag) {
          childTags.push(childTag);
        }
      }
    });

getCssDataForTag という関数はこんな感じでひたすら Figma のレイヤーからデータ取って CSS の語彙に変換しているだけです。

export function getCssDataForTag(node: SceneNode): CSSData {
  const properties: CSSData = {};
  if(!node.visible) return;

  if ('opacity' in node && (node?.opacity || 1) < 1) {
    properties['opacity'] = node.opacity || 1;
  }
  if ('rotation' in node && node.rotation !== 0) {
    properties['transform'] = `rotate(${Math.floor(node.rotation)}deg)`;
  }
  ...他にも色々

▼ 詳細なコード

github.com

ちなみに上記のコードは、昔自力で頑張って作ってた React コード生成プラグインから取ってきて拡張したものです。時を超えて役に立つエモい展開です。

github.com

あとはこうして作った JSON を元にプロンプトを作って書いてもらうだけです。

Convert this Figma JSON object into React, TypeScript, Tailwind code.

## Figma JSON
{ここにさっきの Figma の JSON を貼る}

## constraints
- component does not take any props
- do not omit any details in JSX
- Do not write anything besides code
- import components from @/components directory
- if a layer contains more than 1 same name child layers, define it with ul tag and create array of appropriate dummy data within React component and use map method to render in JSX
- use export rather than default export

以上、基本的な動作の解説でした。

余談: LLM はコード生成において何が革新的か

ちょっとここ数年「デザインからコード生成できないかなぁ。見た目の部分のコーディングしたくないよぉ」という思いから色々調べたり作ったりしてきて想いが乗っていることもあり、若干脇道にはそれてしまいますが LLM がその文脈においてどのような変革をもたらしたかを簡単に語っていきます。

以前、私はFigmaからReactコンポーネントを生成するプラグインを作成し、コミュニティに公開しました。 www.figma.com

このプラグインを作ったはいいものの、以下のような課題がありました。

  • 様々なフォーマットへの対応が非常に手間がかかる
    • 例えば上記のプラグインで Tailwind に対応させようとすると、各プロパティに対してクラス名の変換コードを真心込めて書く必要があります。
  • 既存コードとの競合
    • すでに実装されているページのデザインが新しくなった場合、既存のコードを無視して書いてしまうため、そのままでは使えない状態でした。
  • コードのコンポーネントとの違い
    • 一応上記プラグインでもコンポーネントをセットする機能とかは作っていたのですが、プラグイン専用に情報をインプットする手間が大きいのもあり、あまり現実的な解決策とは言い難い状況でした。

総じて頑張れば実装はできたとは思いますが、恐ろしく手間なのでさすがにやってられんなと感じていました。 そんな折に彗星の如く現れた LLM

プロンプトで

  • 「CSS は Tailwind で書いて」
  • 「これが既存コードなので、この部分は改変せずに書いて」
  • 「これがコードのコンポーネントなのでこれ通りに Props書いて」

と適切なデータと共にお願いするだけで、期待通りの品質のコードを書いてくれるようになりました。 ありがとう OpenAI、こんなに実装のハードルを下げてくれたのであれば、実現に漕ぎ着けるのに努力しない手はありません。

また、ユースケースと相性がいいなと思ったこととして、あくまでこの生成されたコードは後にエンジニアが手直しすることが前提です。

LLM のアウトプットは工夫のしようはあるのですが、どうしても回答にブレだったりハルシネーションが起きてしまいます。なので 90 点のアウトプットを出せればよく、あとは人間がそれを元に開発をロケットスタートできる、というコード生成のようなユースケースは、そういった LLM の特性とも相性が良いように思います。

工夫したこと

以上、基本的なロジックについて解説しましたが、これだけではまだまだ実践には足りない部分があります。

例えば次のようなケースには対応できていません。

  • コンポーネントの Props が Figma 上の Variants のプロパティを元に書いているため、コードだけに存在する Props が書かれない / デザインにだけあるプロパティが書かれてしまう
  • 既に実装したページのロジックを消してしまう
  • OpenAI API にはトークン長の制限があり、大きいレイヤーをコード生成してもらおうとするとエラーになる
  • デザイントークンを使った CSS になっていない

このような課題たちを解くために色々実装してみたので、一つ一つ紹介していこうと思います。

Props のサマリを渡してコードの Props と同じように書いてもらう

ゴールとしては、プロンプトに下記のように使っているコンポーネントのコード上の Props のサマリを渡すことによって「コンポーネントの Props が Figma 上の Variants のプロパティを元に書いているため、コードだけに存在する Props が書かれない / デザインにだけあるプロパティが書かれてしまう」という課題を解決します。

### Button component:

Props:
- variant?: 'primary' | 'secondary' | 'outlined' | 'basic' | 'success' | 'dangerous' | 'dangerousOutlined' | 'dangerousPlain' 
- size?: 'xsmall' | 'small' | 'medium' | 'large'
- subText?: string
- loading?: boolean
- disabled?: boolean
- rounded?: boolean
- startIcon?: ReactNode
- endIcon?: ReactNode
- children: ReactNode

そしてプロンプトに次の一文を足します。

- if Props summaries are provided for the components used in JSON, write props that are required in code props and omit props that only exist in Figma JSON

こうするとコードの Props に応じて Variants の値をうまいこと入れてくれるのと、デザインにしか存在しない Variant Property を書いたりしなくなります。

サマリの作り方

今回はこのサマリを作ってもらうのも OpenAI API にお任せしました。

まず GitHub API で該当のコンポーネントのファイルを取得します。 (ここでははデザインとコードでコンポーネント名が統一されていることを前提としています。)

const { data } = await octokit.repos.getContent({
      owner: 'gaudiy',
      repo: 'design-system',
      path: `packages/designed-components/src/components/${componentName}/${componentName}.tsx`,
    });

そして次のようなプロンプトでサマリを書いてもらいます。ちなみにこちらは gpt-3.5-turbo で十分なタスクなのでそちらを利用しています。

export const buildPromptForPropsSummary = (
  componentName: string,
  codeString: string
) => {
  return `Write Summaries of Props of ${componentName} component in the following code

## Code

\`\`\`tsx
${codeString}
\`\`\`

## constraints
- start with a sentence "{ComponentName} component:"
- write list of props with TS type
`;
};

こうやって作った Props のサマリをコード生成のプロンプトにも入れています。 割と端折って説明しましたが、コンポーネントの情報を取るところは、参照先が一つのディレクトリでは済まないケースがありリクエストの数が多くなってしまうため、もう少し柔軟に検索できるようになると嬉しいなと思っています。ここには Embedding を活用するか、もはやプラグインのコードに埋め込んじゃうか(この場合アプデをどう反映するかは未定ですが)を検討したいなと考えています。

大きいレイヤーの時は Chunk に分割してマージするようにする

前提として OpenAI API はトークン長により制限があります。厳密には違いますが、トークン長は文字数のようなもので、プロンプトが長過ぎるとエラーが起きてしまいます。

なのでそのままだと大量のレイヤーが含まれたページのコードを生成することができないのですが、そんな上限を気にしてプラグインを使いたくない...。 そんな気持ちから Chunk に分割して後でマージする実装をしました。

こんな感じで Tree を Chunk の大きい順にノードを並べて、トークン数が上限値に近い順から元の Tree から取り除いていく関数を作りました。(※現状同じツリー内のものを分割してしまうバグがある)

import {
  APPROX_TOKEN_COUNT_PER_CHARACTER,
  MAX_TOKEN_COUNT_FOR_CODE,
} from '../constants';
import { Tag } from './buildTagTree';

function getTokenCount(tag: Tag) {
  const stringLength = JSON.stringify(tag).length;
  return stringLength * APPROX_TOKEN_COUNT_PER_CHARACTER;
}

export const divideTagTreeToChunks = (tag: Tag): Tag[] => {
  let totalTokenCount = getTokenCount(tag);
  if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
    return [];
  }

  let result: Tag[] = [];

  // 全部の node を探索して {node, childIndex, parentNodeRef} の配列を作り、トークンカウントでソートする
  const nodes: {
    node: Tag;
    childIndex: number;
    parentNodeRef: Tag | null;
  }[] = [];
  const traverse = (
    node: Tag,
    childIndex: number,
    parentNodeRef: Tag | null
  ) => {
    nodes.push({ node, childIndex, parentNodeRef });
    if ('children' in node) {
      node.children.forEach((child, index) => {
        traverse(child, index, node);
      });
    }
  };
  traverse(tag, 0, null);
  nodes.sort((a, b) => getTokenCount(b.node) - getTokenCount(a.node));

  // トークンカウントがmaxChunkSizeより小さいもので多い順に chunk に追加していく
  nodes.forEach(({ node, childIndex, parentNodeRef }) => {
    if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
      return;
    }

    const tokenCount = getTokenCount(node);
    if (
      tokenCount < MAX_TOKEN_COUNT_FOR_CODE &&
      'id' in node &&
      parentNodeRef &&
      'children' in parentNodeRef
    ) {
      result.push(node);
      parentNodeRef.children[childIndex] = {
        nodeId: node.id || '',
        isChunk: true,
      };

      totalTokenCount = getTokenCount(tag);
    }
  });

  return result;
};

そしてプロンプトに子レイヤーが Chunk だった場合に ID だけ JSX に書き込むようにする命令を追加しました。

- if child is chunk, render it as --figmaCodeTransformer=nodeId, it will later be used to replace with another code

そうするとこんな感じで描画してくれます。

<div className="flex flex-col justify-start items-center p-16 gap-16">
          --figmaCodeTransformer=149:20062

そして、Chunk にした部分は JSX だけ書いてもらって、あとは手動で差し替えます。

export function getChunkReplaceMarker(id: string) {
  return `--figmaCodeTransformer=${id}`;
}

export function integrateChunkCodes(
  rootCode: string,
  chunks: { id: string; code: string }[]
) {
  let integratedCode = rootCode;
  chunks.forEach((chunk) => {
    if ('id' in chunk) {
      const replace = getChunkReplaceMarker(chunk.id);
      const regex = new RegExp(replace, 'g');
      integratedCode = integratedCode.replace(regex, chunk.code);
    }
  });

  return integratedCode;
}

// コードをマージするところ抜粋
const codes = await Promise.all([
  createChatCompletion(prompt, []),
  ...chunkPrompts.map((chunkPrompt) => {
    return createChatCompletion(chunkPrompt, []);
  }),
]);

const rootCode = integrateChunkCodes(
  codes[0],
  chunks.map((chunk, index) => ({
    id: 'id' in chunk ? chunk.id || '' : '',
    code: codes[index],
  }))
);

これで動くコードはできあがるのですが、フォーマットが美しくないので後日 prettier 動かす対応は入れようかなと思います。

ちなみにプロンプトでは JSX 部分だけ吐き出してもらう部分はこんな感じで指定しています。Write only JSX だけだと関数宣言の部分も含んじゃったので、例を入れると安定して出力してくれるようになりました。

- Write only JSX
for instance if the result code is like below:
\`\`\`
import { Hoge } from "hoge";
export const ExampleComponent = () => {
    return <div>....</div>
}
\`\`\`
Then output only
\`\`\`
<div>....</div>
\`\`\``;
};

既存のコードを渡すと差分だけ書いてもらう

これはシンプルにページの既存コードを渡して do not change previous code と指定すれば変わった部分だけ追加して出力してくれます。かしこい!

なので「既存コードを選ぶ」という体験をいい感じにするための機能を用意したい気持ちがあります。下記あたりを作れると大体のケースに対応できるのではという予感がしております。

  • テキストエリアに手動コピペ
  • 前に生成したことがあったらファイル名をプラグインに保存しておく
  • レポジトリのファイルをツリー状に表示して選ぶ

デザイントークンをクラス名に変換

今回 Gaudiy 社では Tailwind を使っているので、いわゆるデザイントークンを tailwind.config.js の中で指定している内容通りのクラス名に変換したいなと思っております。

まずはそのデザイントークンの情報を取る方法ですが、Figma ではスタイルという標準の機能Token Studio というスタイルで色やタイポグラフィ以外のものも定義できるプラグインの2パターンが大きくあります。

tokens.studio

とりあえず両方を対応してみます。

スタイルの方は色やタイポグラフィの値が指定されている場合は生の値を渡さず、スタイル名を渡すようにします。

function setColorProperty(
  fills: Paint[],
  colorStyleId: string,
  properties: CSSData,
  colorProp: 'background-color' | 'color'
) {
  if ((fills as Paint[]).length > 0 && (fills as Paint[])[0].type !== 'IMAGE') {
    const style = figma.getStyleById(colorStyleId);
    const paint = (fills as Paint[])[0];

    const color = style ? style.name : buildColorString(paint);
    properties[colorProp] = color;
  }
}

また、TokenStudio はありがたいことに [setSharedPluginData](https://www.figma.com/plugin-docs/api/properties/nodes-setsharedplugindata/) という他のプラグインでも値を参照できる形でデータを保存してくれているので、それを通して取得してみます。

function setFigmaTokens(node: SceneNode, properties: CSSData) {
  const tokenKeys = node
    .getSharedPluginDataKeys('tokens')
    // Omit "version" and "hash" because they are not tokens
    .filter((key) => key !== 'version' && key !== 'hash');

  tokenKeys.forEach((key) => {
    const value = node.getSharedPluginData('tokens', key);
    if (value) {
      // remove css that's represented by token
      if (key === 'itemSpacing') {
        delete properties['gap'];
      }

      properties[key] = value.replaceAll('"', '');
    }
  });
}

こんな感じで取得できたらプロンプトでは「なんかちゃんとした値入ってなかったらそれデザイントークンで Tailwind ではこういうネーミングルールだからよしなに変換してくれ」と指定しておくといい感じになります。

- if string value other than hex or rgb() format is specified for color property, it is design token vairable. it is defined with the name in kebab-case in tailwind.config.js
- if "typography" property is specified, it is defined in tailwind config as typography token that has multiple properties such as font-family, font-size, font-weight, line-height

GraphQL query をレイヤーに保存する

若干「デザインからコード生成」の文脈からは脇道逸れてしまうのですが、Gaudiy では GraphQL を使っており、fragment colocation の文脈でレイヤーやコンポーネントに fragment を保存 -> 生成する時にそれらをまとめた Query を発行、みたいなのできたら良さそうだねという会話があったため、試しに作ってみました。

例えばこうやって二つの違うレイヤーで Fragment を保存して

Fragment を保存する
Fragment を保存する

ルートのレイヤーでプラグインを開くとこのように配下にある Fragment を展開した Query が表示されます。

Fragmentが統合された Query になっている図
Fragmentが統合された Query になっている図

エディタには graphiql-explorer というライブラリを利用しています。

github.com

あまり中身は深く説明してもアレなので概要だけざっくりお伝えすると、この graphiql-explorer はスキーマの内容に応じてポチポチプロパティを選べて便利なのですが、Fragment の記述は対応しておりません。

なのでデータとしては Query として保存しておいて、見た目を表示したりプロンプトにデータを渡す時に Fragment の形に変換してマージするようにしています。

▼ Query を Frament に変換するコード

figma-code-transformer/convertQueryToFragment.ts at main · kazuyaseki/figma-code-transformer · GitHub

▼ Frament をマージするコード

figma-code-transformer/mergeQueryAndFragments.ts at main · kazuyaseki/figma-code-transformer · GitHub

それでレイヤー毎に Fragment や Query を setPluginData で保存して機能させています。 保存する機構は作れたのですが、もう一歩としてはここで書いた gql をプロンプトに使ってデータの繋ぎ込みまで書いてもらったり、ちょっとした工夫としては gql ファイルも一緒に PR 作れるといいのかなと検討しています。

ちょっとした手間を減らす機能

上記以外にもちょっとした手間を減らす工夫を用意したのでサッと紹介していきます。

Storybook も作成

ChatGPT さんはコンポーネントのコードを渡してあげれば Storybook も書いてくれます。

なので下記のようなプロンプトで Storybook も書いていただいてます。(gpt-3.5-turbo で可)

export const buildPromptForStorybook = (
  codeString: string,
  componentName: string
) => {
  return `Write storybook for the following component in Component Story Format (CSF).
\`\`\`tsx
${codeString}
\`\`\`
## constraints
- Do not write anything besides storybook code
- import component from the same directory
- do not have to write stories for componnents used in ${componentName}
`;
};

PR を作るところまでサポート

コードが出力されても、そこから

  • ブランチを切る
  • ファイルを作成
  • そこにコピペ

という手間がまだ存在します。せっかくならそんな手間も減らしたい...。という訳で GitHub API を使って PR を作るところまでサポートしています。

PR を作るコードはこんな感じです。

github.com

ちなみにブランチ名なども下記プロンプトにて ChatGPT さんに考えていただいてます。(gpt-3.5-turbo で可)

export const buildPromptForSuggestingBranchNameCommitMessagePrTitle = (
  codeString: string
) => {
  return `Suggest
- branch name
- commit message
- PR title
- component name
for the following code
\`\`\`tsx
${codeString}
\`\`\`
## constraints
out put as JSON in following property names, do not include new line since this string is going to be parsed with JSON.parse
{ "branchName": "branch name", "commitMessage": "commit message", "prTitle": "PR title", "componentName": "component name" }
`;
};

待ち時間に猫動画を表示する

GPT-4 の API はとても長いです。長いので工夫が必要です。

ただ chatCompletion API は stream で結果を逐次返してくれるので、なんだかんだ ChatGPT 本家のように1文字ずつ表示する方が残りどれくらいかかりそうかもなんとなく分かりますし体験としてはいいのかもと思い始めています。

今後の課題

以上、これまで実現してきたことを書いてきました。

ですがまだまだまだまだ大変なことはありますし、今回作ったものに対して正しい期待値を持っていただくという意味でも、現時点で私が認識している課題を赤裸々に列挙していきます。

パフォーマンス

これは割とどうしようもない感あるのですが、GPT-4 の API はとんでもなく時間がかかります。コード生成に関しては、レイヤーの量にもよりますが 40-120秒ぐらいかかる印象です。猫動画でカバーはしていますが、人は飽きてしまうかもしれません。

こればっかりは OpenAI さんに頑張っていただくしかないのですが、ここが課題として大きくなる場合には

  • 3.5-turbo でも解けるように分解する
  • 同じプロンプトや既に出力したレイヤー構造に対してはキャッシュする
    • これは割とがんばりがいある気がする
  • Web LLM の発展を祈る
  • 待ってる時間を有益に or 楽しく過ごせるような何かを用意する

また、現状はプラグインで実行していますが、デザイナーがフィーチャーのデザインを作り、デザインのリファクタをしたら Figma API を通してまとめて実行しておく、みたいなフローにするとかもありかもしれません。

簡単に構造的なデザインが作られるようにする

本プラグインを使っていく上でネックになるのが「デザインをしっかり作る」ことです。

今回 Figma のレイヤーのデータを元にして OpenAI API さんにコードを書いていただいており、Auto Layout が使われていないとレイアウトの情報が分からなかったり、変なレイヤー構造で作られていると(リストなものが一つのテキストで作られてたりね)、それがそのまま出力されるコードにも反映されてしまいます。

基本的にはデザイン自体のメンテナビリティにも繋がるものなのでしっかり作った方がいいとは思っているのですが、状況によってはそんなに頑張るのかグレーなところもあります。(それもこのコード生成ができるようになった文脈で、しっかり作ることに対するインセンティブが強まったとは思います)(デザイナー目線ではそのメリットは感じられないのでサイロ化されてダメかも)

ちなみに私が思う「しっかり作る」は以下のあたりです

  • Frame 関連
    • No Group, Just use Frame
    • Frame には基本 Auto Layout を使おう(本当に違う場合は除く)
  • コンポーネント関連
    • コンポーネントは命名規則に則って名付ける
    • Variants / Component Properties を全部使っておこう
  • スタイル or/and TokenStudio 全部ちゃんとつけよう
  • レイヤーの構造をコードと同じ形にしよう

この辺りができてたら大体いい感じなると思います。 これをサポートする案をいくつか考えてみます。

DesignLint の拡張

まずはなるべく人間に頼らない方法での解決を考えてみます。

Figma には既に Design Lint という「スタイルがあたってないよ」「規定以外の border-radius の値が入ってるよ」などを教えてくれるプラグインがあります。

www.figma.com

こちらですが OSS として公開されているので、Fork して独自のルールを追加したり機能を足すことができます。

github.com

結構お手軽で、例えば下記は Auto Layout がついていないフレームがあったら怒るルールを足したい場合は下記のような関数を追加して、

export function checkAutoLayout(node, errors) {
  if (node.type === 'FRAME' && node.layoutMode === 'NONE') {
    return errors.push(
      createErrorObject(
        node,
        'frame',
        'Missing Auto Layout',
        'Did you forget it?'
      )
    );
  }
}

controller.ts の Frame に対するチェック関数をまとめているところに追加するだけです。

  function lintFrameRules(node) {
    ...
    checkAutoLayout(node, errors);

    return errors;
  }

また、これだけだと検出しかできないので、eslint fix がごとく勝手に修正できる機能も追加していけば負荷も軽減していけるのかなと思います。

デザインリファクタをワークフローに取り入れる

詳しくは下記記事に記したのですが、デザインはそんなカチカチに作りたくない仮説検証のフェーズと、将来のメンテナンス性のため・コラボレーションのため構造化していくフェーズに分かれると思います。

note.com

後者の時に、しっかりとエンジニアにハンドオフするタイミングでデザインのリファクタをしてから「これは実装 Ready だよ」と伝える決まりにするとかいいかもしれません。

構造的に作らなくてもいい感じのコードを出力してくれるモデルを作る

あとはそもそも Figma をちゃんと作らなくてもちゃんとしたコードが出てくるようになんかできないかという方策です。

一つに OpenAI はいつか画像をインプットにできる、いわゆるマルチモーダルの API を提供するみたいなので、もしかしたら Figma の描画内容を渡してあげたらいい感じになるかもしれません。

また、私はまだ試してはいないのですが、下記のプロダクトが「レイヤー構造を修正することなく」高精度の HTML/CSS を出力することを謳っていたり、

front-end.ai www.figma.com

あとは Hugging Face にも色々あったりするので(下記は試してみましたが期待通りのクオリティではありませんでしたが)オープンのものでも何かしら出てくるかもしれません。

huggingface.co huggingface.co

こういったものを組み合わせると幸せになれそうです。 ちょっと ML 素人の私が自分で作るのを試すのはちょっと時間がかかりすぎるかもしれないので、これを見てる誰か、作ってくれ!!

スタイルレスな UI Kit を使う

Figma Community にはスタイルレスで構造のみを持った UI Kit が色々作られています。 例としては下記のようなものがあります。

www.youtube.com

tokens.studio

一方でこれに則って作る手法を学習するくらいなら、もはや Framer などのコードにエクスポートすることを最初からゴールにしているデザインツールを使った方がいいんじゃないか感がなくもないです。 www.framer.com

Figma で表現できないものはスタイルを付与できない

大体のものが実現できるようになった Figma ですが、頻出 CSS としてまだ足りない感があるものとしては次のものがあります。他にも細かいものは色々あるとは思いますが大きいものだとこれくらいだと思います。

  • flex-wrap
  • table layout
  • grid layout

情報がないものはないのでどうしようもなく、Figma に頑張っていただくしかありません。 幸い flex-wrap は Auto Layout v5.0 なるもので登場しそうで(下記はおそらく早めに権限をもらった方)

また、table に関しては既に FigJam で登場しており、これは Figma にもコピペして使えることからその内 Figma でもリリースされるだろうと予感しています。

www.figma.com

という訳で課題はありますが、デザインツール由来の実装のものとのズレはほぼほぼなくなるんじゃないかなと勝手に期待しています。

同じページで複数のフレームがある場合の対応

実際のデザインでこのプラグインを使い始めて「あーこれどうしよう?」と思ったのが "同じページで複数のフレーム" がある場合です。

例えば下記のように、同じページ内でローディング表示したりモーダルとかがにゅっと出たりして、デザインが複数のフレームで表現されることはよくあることです。

Home画面のデザインとHome画面にモーダルが表示されている図

人間が差分に対してだけプラグインを実行して手動コピペでもいいのですが、せっかくならフレームをまとめて選択したら状態に応じてだし分けるコードも書いてくれたらテンション爆上げだと思うので、何かしら実現方法を考えてみたいと思っています。

より良いデザインプロセスを問い続ける

ここまでガッツリこのプラグイン開発記について語ってきたのですが、正直な気持ちを吐露しますと、これを作っていて「そもそもデザインを Figma でやる必要はあるのか?」という問いはしっかり考えて行った方がいいなと感じました。

より具体的に言うと、「このきっちりデザインを作る作業はコーディングするコストと変わらないのでは」だったり「そもそもデザイン案を直接コードで表現してもいいのではないか?」という問いです。

一点目に関していうと、より良いコード生成のアウトプットのためにはデザインをより構造的に作ったり、メタデータの付与などを頑張りたくなるのですが、独自にデザインに対してルールを覚えるくらいなら諦めてコードで書いた方が良くね?という線引きはどこかで生じると思います。

「デザインからコードを生成する」という目的に捉われすぎて全体のプロセス改善に繋がらなくては本末転倒なので、「デザインをきっちり作る」ことはコード生成の文脈のみならず、デザイン自体のメンテナビリティだったりあるいは他の目的にもプラスに働くものであるかは、常に冷静に判断すべきでしょう。

一方別の話で、 LLM によってデザインプロセス自体が大きく変わる可能性にも柔軟に開いていたいなと考えています。

例えばですが、現状ではデザイナーがスクラッチで(コンポーネントやデザインシステムのルールに則ったりはあると思いますが)デザインを作っていってると思いますが、もしかしたら今後は作りたい仕様・ユースケースと既にあるデザインをインプットにしたら AI がデザイン案を出してくれて、それをちょっとチューニングして実装するUIデザインのプロセスが訪れるかもしれません。

そして、その時、AI がアウトプットするデザインは Figma のようなデザインツールで表現される必要はありません。実際にユーザが触れるコードで出力をしてしまえばいいのです。

Magician などの AI を活用した Figma プラグインの数々を作ってきた jsngr さんは既にそういった AI を前提にした新しい体験のデザインツールを作り始めているように見えます。

こういった新しい体験のデザインプロセスになった時、今この記事で私が解説した Figma を前提にしたコード生成プラグインは価値が無になりますが、サンクコストに縛られず、常により良いプロセスを模索していきたいなと思います。

おわりに

以上、ChatGPT を使ってコードを生成する取り組みについて語ってきました。

私個人としては長年の夢だった見た目部分のコードを書かなくてよくなる体験がグッと近づいてきた感がありワクワクする毎日です。

一方で課題もたくさんあるので Gaudiy で試していきたいのはもちろん、色んな企業の人でこちらを試していただいてより良いデザイン - 実装プロセス作りの知見を共有していけるとなおのこと楽しいかなと思い、こちらのプラグインを OSS として作りました。

ぜひお試しいただいて、良かったこと辛かったことをご共有いただけると大変嬉しいです。

そして最後に宣伝コーナーです。

Gaudiy では絶賛プロダクト開発人材を募集中です。会社としてLLMに投資しており、エンジニアもデザイナーも特に AI を活用した業務改善、所謂 AIOps にもかなり力を入れています。こういった取り組みにワクワクする方は楽しく働けるのではないかなと思います。私はまだ入社してないので多分。

recruit.gaudiy.com

それでは!

参考資料

www.figma.com community.openai.com www.pinecone.io