Skip to content

ワークフロー

English | 日本語

Yosegi は 2 方向に走ります。 上流では登録済みのコンポーネントから Story を組み立て、下流ではその Story を実ページへ転換します。

必須なのは registry buildcomponent list / inspect で、図の「経路」から先はこのページが順に説明する選択になります。

ユースケース

想定する読み手は、既に Storybook 上のデザインシステムを持つプロダクトチームです。Yosegi を叩くのは人ではなくコーディングエージェントで、人は普段の言葉でエージェントに頼みます。

ユースケース 1: 画面モックを速く作る

  • 人が言うこと: 「うちのコンポーネントでクーポン管理画面のモックを作って」
  • エージェントがやること: Registry を作り、使うコンポーネントを inspect し、ホストの規約を読み、Story を書きます。 直接書くか、静的な画面であれば検証の恩恵を取りに Screen JSON を経由するかを選びます。
  • 成果物: ホストの Storybook にそのまま置ける .stories.tsx。 実物のコンポーネントと実物の props で描かれ、ホスト自身の型検査を通っています。

修正も同じ会話で進みます。 「ここを 3 カラムにして」「ここは Card ではなく Table で」。 エージェントは Story か Screen JSON を直して出し直すだけです。

ユースケース 2: 承認されたモックを実装へ移す

  • 人が言うこと: 「このモックを実装して」
  • エージェントがやること: Story とホストのページ規約を読み、モックの値を実データへ繋ぎながら実ページを書きます。Yosegi が生成した Story なら、先に実装コンテキストを取って tasks[] を 1 つずつ潰せます。
  • 成果物: 実ページの実装。 レビュアーが承認したのと同じコンポーネントで組まれています。

入力は Story ファイルだけなので、モックを組んだのが別の人・別のセッションでもかまいません。 ただし story import に手を伸ばす前に「下流」の制約を読んでください。

ユースケース 3: Figma を介さずイテレーションする

レビューで見るものと実装で使うものが同じコンポーネントになります。

  • デザインシステムに無い見た目は、そもそも組めません。Registry に無いコンポーネントは検証で弾かれます。
  • 承認された案がそのままユースケース 2 の入力になるので、「実装したら別のコンポーネントだった」というズレが起きません。
  • 逆に、既存のコンポーネントでは足りないことが早く分かります。 実装中の不意打ちではなく、デザインシステムへの要望として出てきます。

新しいビジュアル表現を決めるのは引き続き Figma の仕事であり、Figma 自身の MCP も Code Connect を通じて既存のコードへ踏み込んでいます。 どちらか一方を選ぶ話ではありません。 Yosegi が扱うのは、いま手元にあるコンポーネントで組める画面を、JSX が 1 行も無い段階で Registry と突き合わせて見せるところまでです。

上流: Story を組み立てる

registry buildcomponent list / inspect → Story。

必須なのは最初の 2 段で、ここで実際の props を確定させ、推測した prop を出荷しないようにします。 画面の骨格や合成の作法は Registry ではなくホスト自身の Story やテンプレートから得ます。Story をどう書くかは形式の選択でしかありません。 画面上のどれか 1 つでも JSON の形を持たない値を必要とするなら(ランタイムのオブジェクト、コンポーネント参照、式で組む ReactNode、条件分岐)、Screen JSON には表現する構文が無いので直接書きます。 モックデータ・繰り返し・画面状態は表現できます。fixtures が binding の参照する名前付き JSON 値を運び、repeat がサブツリーを一覧の行へ展開します。variants は画面のほかの状態(ローディング / エラー / 空)を追加の Story export として出力します(Screen JSON を参照)。 そうでなければ Screen JSON は、JSX を 1 行も書く前の検証と、下流が読み戻す引き継ぎコメントをくれます。

どちらの経路でも結果を確定させるのはホストの型検査です。JSX を実物の型と突き合わせるので、Registry に対する検証より多くを見ます。

screen generate は書き出す前に検証します。 エラーがあれば何も書かず、エラーの配列とともに終了コード 1 で終わります。1 件ずつ直すのではなく、配列全体を反映してから再実行します。

$ yosegi screen generate tmp/screen.json --out ... --data-dir .yosegi
[
  {
    "nodeId": "card",
    "path": "$.children[0]",
    "code": "INVALID_PROP_VALUE",
    "message": "Value for \"app/components/ui/card#Card.elevation\" does not match kind \"enum\" (received: \"float\").",
    "suggestion": "Use one of: \"flat\", \"raised\""
  },
  {
    "nodeId": "cta",
    "path": "$.children[1]",
    "code": "COMPONENT_NOT_FOUND",
    "message": "Component \"Button\" is not registered.",
    "suggestion": "Did you mean: app/components/ui/button#Button?"
  }
]

COMPONENT_NOT_FOUNDUNKNOWN_PROPUNKNOWN_BINDING_TARGET にはレーベンシュタイン距離で最も近い候補が付きます。INVALID_PROP_VALUE には受け取った値と enum の選択肢が付きます。 どのエラーもツリー内の位置を示す path を持つので、id が衝突していてもノードを特定できます。variant の木で起きた issue はさらに variant 名を持つ variant を運び、その path は variant の operations 適用後の木を指します。 警告は生成を止めず、ファイル書き出しの後に並びます。

生成される meta は既定では title だけです。 ホストが要求する定型は --meta-template <file> で差し込みます。 値はソースの断片として解釈せずに引き継ぐので、Yosegi が持っていない Figma の URL を捏造することはありません。 ただしテンプレート元にした既存 Story の URL はそのまま引き継がれ、警告で名指しされます。

Storybook が無い場合

Storybook を持たないホストでは、screen generate --target component が同じ画面を CSF ではなく素の React コンポーネントファイルとして書き出します。 検証・fixturesrepeatvariants の挙動は同一で、各状態は Story export ではなく export された関数になります。 こうしたファイルをどこでレビューするかを Yosegi は規定しません。 レビューはホストの型検査に加えて、利用者が選んだ確認手段(たとえば作業用のルートにコンポーネントを描画する)で行うものなので、エージェントは推測せずに確認します。 知っておくべき非対称が 1 つあります。story import が読めるのは Story だけなので、コンポーネントファイルは Screen JSON へ読み戻せません。 画面をあとで直す可能性があるなら Screen JSON を残しておきます。

どちらのターゲットも同じレンダラと同じ import プランを通るので、JSX は同一になります。

検証エラーの code

どのエラーも機械可読な code と、直し方を決められるだけの suggestion を持ちます。

code意味直し方
COMPONENT_NOT_FOUNDその id は Registry に無いsuggestion の候補に差し替える。候補が無ければ component list --query で探し直す
UNKNOWN_PROPその prop は無いsuggestion の名前に直すか、component inspect で実在する props を並べる。ノード直下に置くべきフィールド(bindings / events / when / each / repeat)を props の中に書いた場合もここに落ち、ノードへ移す suggestion が付く
UNKNOWN_BINDING_TARGETbindings のキーが存在しない prop を指しているsuggestion の名前に直す。ReactNode の prop は prop ではなく slot なので binding の宛先にはできない
INVALID_PROP_VALUE値が型や enum と合わない。message に受け取った値が入るsuggestion の選択肢から選ぶ
MISSING_REQUIRED_PROP必須 prop に値が無い。message に kind が入る値を入れる(enum なら suggestion に選択肢が付く)。binding だけで満たせるのは式が識別子パスの場合のみ
FUNCTION_PROP_VALUE関数型の prop に値を書いている宣言を events(または bindings)へ移し、props から消す
RESERVED_PROPprops の中に childrenkeyref を書いているこれらは JSX の属性として出力されない。内容は slots.children へ移し、keyref は消す
SLOT_NOT_FOUNDその slot は無いcomponent inspect で slots を確認する。子要素はたいてい children
SLOT_COMPONENT_NOT_ALLOWED / SLOT_MAX_ITEMS_EXCEEDEDslot の制約がその子を許さない許されるものは suggestion にある
PARENT_NOT_ALLOWED / CHILD_NOT_ALLOWED親子の組み合わせに制約がある許されるコンポーネントは suggestion にある
DUPLICATE_NODE_ID2 つのノードが同じ id を持つ。message に衝突した両方の path が入る。repeat 展開の -1-N サフィックスが既存 id と衝突する場合もここに落ちるどちらかを変える
REPEAT_ON_ROOTルートノードに repeat がある。複製を収める親 slot が無いコンテナノードで包み、子へ repeat を付ける
REPEAT_OUT_OF_RANGErepeat が 2〜20 の整数でない数を直すか、1 つで足りるなら repeat を消す
REPEAT_EXPANSION_TOO_LARGEすべての repeat を展開すると 2000 ノードを超える。ネストした repeat は掛け算で増える数を下げるか、ネストを解く
VARIANT_OPERATION_FAILEDvariant の operations をベースの木へ適用できなかった。message に失敗した operation の code が入るoperation が指すすべての nodeId がベースの木に存在する(または同じ variant の先行 operation が追加した)ように直す

生成を止めない警告:

code意味
REGISTRY_VERSION_MISMATCH画面が参照する Registry version が、使っている Registry と違う
MISSING_REQUIRED_SLOT必須の slot が空。モックでは意図的なことが多い
BOUND_REQUIRED_PROP必須 prop が binding だけで宣言されている。Story には prop={<式>} が出るため、その名前が Story に存在するまで型検査を通らない。binding の先頭が fixture 名なら実在するので出ない
UNUSED_FIXTUREどの binding も参照しない fixture。出力はされる。たいていは binding のリネームか削除の名残
NOT_EDITABLE_PROP_VALUEnot-editable な prop に値を書いている。形が検証されないまま Story へ出る
UNKNOWN_EVENT_TARGETevents のキーが Manifest に無い prop を指している。Manifest がイベントの一覧を持たないため警告どまり
SYNTHETIC_NAME_SHADOWED短い id が合成プリミティブに解決されたが、Registry にも同名のホストコンポーネントがある
DEPRECATED_COMPONENT非推奨のコンポーネント

INVALID_REQUEST はこれらとは別物です。 スキーマを満たしていないファイルでは検証に到達せず、代わりに { "error": { "code": "INVALID_REQUEST", "issues": [...], "hints": [...] } } が返ります。issuespath が問題箇所を指し、hints が正しい形を示します。 まず形を直してから上のループに入ります。

下流: Story を実装へ転換する

story importscreen context → ホストの規約に従って実装。

この経路が機能するのは screen generate が書いた Story に対してだけです。story import は Story を TypeScript の AST で解析しますが、現実の Story がコンポーネントツリーを直書きすることはまずありません。 通例は render が隣のファイルで定義したラッパーコンポーネント 1 つを返す形です。 その Story は 1 ノードに、しかも読めない構文が無い以上は警告 0 件で import されます。 画面が抜け落ちていることを知らせるものは何もありません。 手書きの Story が相手なら、Story そのものを読んでください。

screen generate が書いた Story は、ノード id を除いてそのまま読み戻ります。 形が実行時にしか決まらない構文は warnings に載り、読めた範囲のツリーは返ります。

screen context は Screen JSON を実装コンテキストへ展開します。 実装中に効くのは主に 4 つです。

  • imports: そのまま貼れる import 文。CSF エミッタと同じ import 計画から出るので、生成された Story と食い違うことがありません。
  • structure.outline: 入れ子をインデントした行で表したもの。
  • components[]: 使っているコンポーネントごとの usedProps / usedSlots / manifest / importStatement。 合成プリミティブと未登録の id には印が付きます。
  • tasks[]: bindingsevents を結線タスクへ平坦化したもの。 各タスクは nodeId$.children[1] 形式の path を持ちます。

残り(requirements / target / implementation / screen)は補助情報です。

story import の警告

解析はソースの AST だけで行うため、形が実行時にしか決まらない構文は読めません。 ツリーを 1 つも復元できないときは、下記 2 つのコードのいずれかを error.code に、警告の全リストを error.warnings に入れた標準のエラーエンベロープを返し、exit 1 で終わります。 それ以外では読めないノードに印を付けて先へ進みます。ノード・prop・intent を名指しする警告は、その内容が Screen JSON に入っていないことを意味しますので、その部分は元の Story を直接読みます。TITLE_NOT_STATICMULTIPLE_ROOTSMULTIPLE_STORIES のような情報通知は内容を保ったまま、何が変わったかだけを伝えます。warnings が空であること自体は何の証明にもなりません。Story と突き合わせてノード数を数えてください。

code意味
STORY_NOT_FOUNDrender 関数を持つ export が無い(component + args 形式の Story がここに落ちる)か、--story-name がどの export とも一致しない(メッセージに候補が並ぶ)。exit 1 で何も取り込まれない
RENDER_NOT_STATIC選ばれた export の render が静的に読めない。--story-nameargs のみの export を指したときもここ。exit 1 で何も取り込まれない
TITLE_NOT_STATICmeta の title が静的な文字列でない。画面名がフォールバックするだけで取り込みは続く
OPAQUE_EXPRESSION{items.map(...)} や条件分岐など、静的に読めない式。そのノードは落ちる
OPAQUE_PROPprop の値が読めない(変数参照など)。その prop だけが落ちる
OPAQUE_ELEMENT対応する合成プリミティブが無い DOM タグ。Box として残るがタグ名は失われる
SPREAD_ATTRIBUTE{...args} は展開できない
INTENT_NOT_APPLIEDintent コメントの直後に兄弟要素が複数あり、bindingsevents を付ける先を決められず落ちた
OPAQUE_FIXTUREトップレベルの const の初期化子が JSON リテラルでなく、fixture として読み戻せなかった
COMPONENT_NOT_RESOLVEDimport 文から Registry の id へ辿れない。ノードはローカル名のまま残るので、検証が候補を出す
COMPONENT_AMBIGUOUS同じ export 名の候補が複数ある。--import-map で絞る
IMPORT_PATH_MISMATCHexport 名は一致するが import 元が Registry と違う。Registry が古い可能性
MULTIPLE_ROOTSルート要素が複数あったため Box で包んだ
MULTIPLE_STORIES取り込んだものより多くの Story をファイルが export している(典型は variants のファイル)。1 回の実行で読むのは 1 export。別の export は --story-name で読む。差分が variants へ再構成されることはない

次に読む

MIT License のもとで公開されています。