開発者が、distや型定義がないSPAのUIコンポーネントをAIデザインツール「Claude Design」に同期させるフローを図解。synth-entryモードで課題を解決し、デザインツールにコンポーネントを取り込む過程を示しています。

AI デザインツールにコンポーネントを取り込もうとすると、最初の壁は「npm ライブラリではない普通の SPA をどう見せるか」です。本記事では、ビルド成果物を持たない React SPA の UIコンポーネントを Claude Design に同期させる synth-entry モードの実践手順を、実際にハマったポイントとあわせて紹介します。

こんな人におすすめ

  • 自社の React/TypeScript SPA を AI デザインツールに取り込みたい方
  • npm パッケージ化していないアプリのコンポーネントをデザインシステムとして扱いたい方
  • Claude Design や類似のコンポーネント同期ツールで ENAMETOOLONG や正規表現由来のエラーに遭遇している方

課題: SPA には dist も .d.ts もない

公開された npm ライブラリであれば、dist/ にビルド済みの JS と .d.ts 型定義が同梱されています。デザインツールはそれらを読み込めば済みます。

ところが普通の React SPA は事情が違います。配布物ではないので dist/ ビルドを持たず、型定義ファイルも生成しません。src/components/.tsx がそのまま置かれているだけです。これでは AI デザインツールが「パッケージ」として認識できず、直接の取り込みができません。

解決策: synth-entry モードで擬似パッケージに見せる

synth-entry モードは、src/components/ を「あたかも npm パッケージであるかのように」見せかけるアプローチです。アプリのソースをそのまま擬似パッケージの入口として扱い、デザインツール側にはパッケージの体裁だけを提供します。

ポイントは、.design-sync/ 配下に同期用のファイルを置くだけで完結する点です。アプリ本体のコードや挙動は一切変更しません。同期のための足場を脇に用意するイメージです。

symlink-to-root は ENAMETOOLONG で即死する

最初に試したくなるのが「リポジトリのルートを node_modules 内にシンボリックリンクで丸ごと見せる」方法です。しかしこれは ENAMETOOLONG で破綻します。ルートには node_modules 自身が含まれるため、解決経路が循環的に膨れ上がり、パス長の上限を超えてしまうのです。

そこで採用するのがスクラッチパッケージパターンです。必要なディレクトリだけを個別に symlink した、最小構成の擬似パッケージを node_modules 内に手で組み立てます。

# スクラッチパッケージの初期化
rm -f node_modules/myapp
mkdir -p node_modules/myapp
printf '%s' '{ "name": "myapp", "version": "1.0.0", "type": "module", "private": true }' \
  > node_modules/myapp/package.json
ln -sfn ../../src node_modules/myapp/src
ln -sfn ../../shared node_modules/myapp/shared

ルート全体ではなく srcshared だけをリンクすることで、循環を断ち切りつつ必要なソースへ到達できます。

synth-entry 用の package.json に main / exports / types を書いてはいけない

ここが直感に反するところです。通常のパッケージなら mainexportstypes を書いてエントリポイントを指すのが定石ですが、synth-entry ではこれらを書きません

これらのフィールドを書くと、ツールは「ビルド済みのエントリがある」と判断し、存在しない成果物(dist/index.jsindex.d.ts)を解決しに行ってしまいます。SPA にはそれが無いので解決に失敗します。synth-entry モードはソースを直接読む前提なので、エントリ宣言をあえて空にしておくのが正解です。上記の初期化スクリプトで nameversiontypeprivate だけにしているのはこのためです。

tsconfig の paths に /* が含まれるとコンバータが壊れる

もう一つ厄介なのが tsconfig の paths です。コンバータ内部の正規表現が /* を含むパターンを正しく扱えず、エイリアス解決が壊れるバグがあります。

回避策は、同期専用にクリーンな tsconfig を用意し、paths を最小限に絞ることです。

{
  "compilerOptions": {
    "baseUrl": "..",
    "paths": {
      "@/*": ["./src/*"],
      "@shared/*": ["./shared/lib/*"]
    }
  }
}

アプリ本体の tsconfig には手を入れず、.design-sync/ 配下にこの専用設定を置くだけにとどめます。エイリアスは実際に使っているものだけを並べ、余計なパターンを持ち込まないのがコツです。

非視覚コンポーネントは componentSrcMap: null で除外する

src/components/ には、見た目を持たないコンポーネントも混ざります。ルーティング制御の AdminRoutes、計測用の GoogleAnalyticsRoute、スクロール制御の ScrollToTop などです。これらをデザインツールに渡してもプレビューできず、ノイズになるだけです。

そこで componentSrcMap で対象を null に指定し、同期対象から外します。

{
  "shape": "package",
  "srcDir": "src/components",
  "componentSrcMap": {
    "AdminRoutes": null,
    "GoogleAnalyticsRoute": null,
    "ScrollToTop": null
  }
}

視覚的に意味のあるコンポーネントだけを取り込むことで、デザイン側の一覧がすっきりします。

グローバルCSS・デザイントークンはバンドルに乗らない

最後に理解しておくべき制約があります。synth-entry で取り込まれるのは個々のコンポーネントのソースであり、グローバルに適用される CSS やデザイントークンはバンドルに含まれません。

つまりデザインツール上のプレビューは、アプリ実機の見た目と完全には一致しないことがあります。色やスペーシングのトークンがアプリ全体に効いている場合、その部分は再現されない前提で確認する必要があります。これは欠陥ではなく仕様として割り切り、コンポーネント単位の構造確認に用途を絞るのが現実的です。

まとめ

  • SPA は dist/.d.ts を持たないため、そのままでは AI デザインツールに取り込めない
  • synth-entry モードは src/components/ を擬似パッケージとして見せかけて解決する
  • ルート丸ごとの symlink は ENAMETOOLONG で破綻するので、必要なディレクトリだけを symlink するスクラッチパッケージパターンを使う
  • 擬似パッケージの package.json には mainexportstypes を書かない(存在しない成果物を解決しに行ってしまうため)
  • tsconfig の paths/* が含まれるとコンバータの正規表現バグで壊れるので、同期専用のクリーンな tsconfig を用意する
  • 非視覚コンポーネントは componentSrcMapnull にして除外する
  • グローバルCSS・デザイントークンはバンドルに乗らないため、プレビューは実機と完全一致しない前提で扱う
  • すべて .design-sync/ 配下で完結し、アプリ本体のコードや挙動は一切変更しない