
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` 内に手で組み立てます。
```sh
# スクラッチパッケージの初期化
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
```
ルート全体ではなく `src` と `shared` だけをリンクすることで、循環を断ち切りつつ必要なソースへ到達できます。
## synth-entry 用の package.json に main / exports / types を書いてはいけない
ここが直感に反するところです。通常のパッケージなら `main`・`exports`・`types` を書いてエントリポイントを指すのが定石ですが、`synth-entry` ではこれらを**書きません**。
これらのフィールドを書くと、ツールは「ビルド済みのエントリがある」と判断し、存在しない成果物(`dist/index.js` や `index.d.ts`)を解決しに行ってしまいます。SPA にはそれが無いので解決に失敗します。`synth-entry` モードはソースを直接読む前提なので、エントリ宣言をあえて空にしておくのが正解です。上記の初期化スクリプトで `name`・`version`・`type`・`private` だけにしているのはこのためです。
## tsconfig の paths に /* が含まれるとコンバータが壊れる
もう一つ厄介なのが tsconfig の `paths` です。コンバータ内部の正規表現が `/*` を含むパターンを正しく扱えず、エイリアス解決が壊れるバグがあります。
回避策は、同期専用にクリーンな tsconfig を用意し、`paths` を最小限に絞ることです。
```json
{
"compilerOptions": {
"baseUrl": "..",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["./shared/lib/*"]
}
}
}
```
アプリ本体の tsconfig には手を入れず、`.design-sync/` 配下にこの専用設定を置くだけにとどめます。エイリアスは実際に使っているものだけを並べ、余計なパターンを持ち込まないのがコツです。
## 非視覚コンポーネントは componentSrcMap: null で除外する
`src/components/` には、見た目を持たないコンポーネントも混ざります。ルーティング制御の `AdminRoutes`、計測用の `GoogleAnalyticsRoute`、スクロール制御の `ScrollToTop` などです。これらをデザインツールに渡してもプレビューできず、ノイズになるだけです。
そこで `componentSrcMap` で対象を `null` に指定し、同期対象から外します。
```json
{
"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` には `main`・`exports`・`types` を書かない(存在しない成果物を解決しに行ってしまうため)
- tsconfig の `paths` に `/*` が含まれるとコンバータの正規表現バグで壊れるので、同期専用のクリーンな tsconfig を用意する
- 非視覚コンポーネントは `componentSrcMap` で `null` にして除外する
- グローバルCSS・デザイントークンはバンドルに乗らないため、プレビューは実機と完全一致しない前提で扱う
- すべて `.design-sync/` 配下で完結し、アプリ本体のコードや挙動は一切変更しない

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
ルート全体ではなく src と shared だけをリンクすることで、循環を断ち切りつつ必要なソースへ到達できます。
synth-entry 用の package.json に main / exports / types を書いてはいけない
ここが直感に反するところです。通常のパッケージなら main・exports・types を書いてエントリポイントを指すのが定石ですが、synth-entry ではこれらを書きません。
これらのフィールドを書くと、ツールは「ビルド済みのエントリがある」と判断し、存在しない成果物(dist/index.js や index.d.ts)を解決しに行ってしまいます。SPA にはそれが無いので解決に失敗します。synth-entry モードはソースを直接読む前提なので、エントリ宣言をあえて空にしておくのが正解です。上記の初期化スクリプトで name・version・type・private だけにしているのはこのためです。
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 には main・exports・types を書かない(存在しない成果物を解決しに行ってしまうため)
- tsconfig の
paths に /* が含まれるとコンバータの正規表現バグで壊れるので、同期専用のクリーンな tsconfig を用意する
- 非視覚コンポーネントは
componentSrcMap で null にして除外する
- グローバルCSS・デザイントークンはバンドルに乗らないため、プレビューは実機と完全一致しない前提で扱う
- すべて
.design-sync/ 配下で完結し、アプリ本体のコードや挙動は一切変更しない