spree-dashboard-plugins について
Spree ダッシュボードプラグインのパッケージ化、インストール、公開を扱う開発スキルです。プラグインのひな形、レジストリ、ファイルルート、ホスト側の検出、対応する Rails 拡張について説明します。
主な機能
- 01ダッシュボードプラグインのひな形生成
- 02ダッシュボード拡張の登録
- 03パッケージ内ファイルルートの構成
- 04ホストのプラグイン検出設定
- 05Rails 拡張バックエンドの構築
- 06npm パッケージの公開とバージョン管理
活用シーン
- 01複数店舗向けダッシュボード機能のパッケージ化
- 02プラグインページとナビゲーションの追加
- 03ダッシュボード UI と拡張エンドポイントの接続
- 04npm 配布用プラグインの準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-dashboard-pluginsインストール後、Claude Code で/spree-dashboard-pluginsを入力して読み込まれたか確認します。
詳細
Spree Dashboard プラグイン — パッケージ化と配布
ダッシュボードプラグインは、インポート時に defineDashboardPlugin({...}) を呼び出すエントリーモジュールを持つ npm パッケージです。アプリ内のカスタマイズとまったく同じレジストリ(nav、settingsNav、routes、slots、tables、formFields、customFieldComponents。詳細は spree-dashboard を参照)を使用します。異なるのはパッケージ化の方法です。ピア依存関係、検出、ファイルルート、公開が変わります。
初めからプラグインにしないでください。 ご自身のストアで使う場合は、コードを apps/dashboard/src/plugins.ts に配置してください。複数のストアやプロジェクトでインストールする場合(オープンソースの連携、代理店による展開、商用アドオン)のみ、パッケージ化してください。後からアプリ内コードをプラグインに移行する作業は、ほとんどコピーだけで済みます。
ダッシュボードは Developer Preview(@spree/dashboard-* の 1.0.0-beta.x)です。リリースをこまめに追う必要があります。
スキャフォールド
npx @spree/cli plugin new reviews \
--npm-scope @acme --ruby-name spree_reviews --module-name Reviews \
--author "Ada Lovelace" --author-email [email protected] --license MIT
# -y / --yes accept defaults for anything not passed (author/email from git config)
# --no-install skip pnpm install --force overwrite a non-empty directory
# --no-dashboard / --no-engine reserved for when engine scaffolding lands (--no-dashboard leaves nothing to scaffold today)
ライセンス:MIT(デフォルト)、Apache-2.0、BSD-3-Clause。生成される構成:
reviews/
├── package.json # workspace root (pnpm -r build/lint/typecheck/test), private
├── pnpm-workspace.yaml
├── LICENSE, README.md, .gitignore
└── packages/dashboard/
├── package.json # @acme/reviews-dashboard — marker + peerDependencies
├── tsconfig.json # excludes src/routes (see File routes)
├── biome.json
└── src/
├── index.tsx # i18n bundle + defineTable + defineDashboardPlugin (nav + product slot)
├── client.ts # adminClient.request wrappers for /reviews
├── types.ts
├── pages/reviews-list.tsx
├── routes/reviews.index.tsx # TanStack file route → /$storeId/reviews
├── slots/product-reviews-card.tsx
└── locales/en.json
ダッシュボードパッケージの名前は <scope>/<name>-dashboard です。CLI がスキャフォールドを生成するのはダッシュボード側だけです。生成された README にその旨が記載され、拡張ガイドへのリンクも示されます。サンプルページは /api/v3/admin/<name> エンドポイントを呼び出しますが、バックエンド側を別の拡張 gem として構築するまでは存在しません(下記を参照)。スキャフォールドで生成されるパッケージの build スクリプトは tsc --noEmit です。パッケージに含まれる TypeScript ソースの型チェックを行いますが、JavaScript は出力しません(下記の「ビルド済みとソース」を参照)。
エントリーモジュール
// packages/dashboard/src/index.tsx — imports from -core/-ui, NEVER from @spree/dashboard
import { defineDashboardPlugin, defineTable, i18n } from '@spree/dashboard-core'
import { MessageSquareIcon } from 'lucide-react'
import en from './locales/en.json'
import { ProductReviewsCard } from './slots/product-reviews-card'
i18n.addResourceBundle('en', 'translation', en, true, true) // before anything calls i18n.t
defineTable('acme-reviews', { title: i18n.t('admin.acme_reviews.title'), columns: [/* … */] })
defineDashboardPlugin({
nav: [{ key: 'acme-reviews', labelKey: 'admin.acme_reviews.nav', path: '/reviews', icon: MessageSquareIcon, position: 650, subject: 'Spree::Review' }],
slots: { 'product.form_sidebar': [{ id: 'acme-reviews-card', component: ProductReviewsCard as never, position: 250 }] },
// pages ship as file routes, not `routes:` entries
})
@spree/dashboard ではなく -core/-ui を使う理由:プラグインはホストのシェルに組み込まれます。シェルパッケージをインポートすると、アプリ全体がパッケージに取り込まれ、使用するダッシュボードのバージョンも固定されてしまいます。
すべてに名前空間を付けてください。レジストリキー、スロット ID、テーブルキー(acme-reviews)、ロケールキー(admin.acme_reviews.*)、バックエンド API パスが対象です。キーが重複すると、利用側のアプリの起動時に競合するキーを示してエラーになります。
ファイルルート
パッケージ化されたプラグインは、TanStack のファイルルートとしてページを提供します。ホストの型付きルートツリーにコンパイルされ、型付きの <Link>、コード分割、validateSearch、ローダーを利用できます。マーカーでディレクトリを宣言してください。
// packages/dashboard/package.json
"spree": { "dashboard": { "plugin": true, "routes": "./src/routes" } }
// src/routes/reviews.$reviewId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { ReviewPage } from '../pages/review'
export const Route = createFileRoute('/_authenticated/$storeId/reviews/$reviewId')({
component: () => <ReviewPage id={Route.useParams().reviewId} />,
})
- 最終的に合成されたパスリテラル(
/_authenticated/$storeId/…)をコミットしてください。ジェネレーターが検証します。 - ホストは、インストール済みバージョンをもとに、開発の開始時とビルドのたびに
src/routeTree.gen.tsを再生成します。プラグインをアップグレードすると、開発サーバーの再起動後にそのルートが追加されます。 - パッケージ単体の
tscではsrc/routesを除外してください(スキャフォールドでは除外されます)。ルートファイルの型チェックはホストのプログラム内でのみ行えます。パッケージをリンクしたホスト(create-spree-appプロジェクトまたはdashboard-starterのチェックアウト)を使って開発してください(pnpm add ../reviews/packages/dashboardまたはワークスペース)。 - ファイルルートはレジストリディスパッチャーを経由しないため、
subjectによる制御は適用されません。コンポーネント内で<Can>/usePermissions()を使って制御してください(これは UX のみを対象とし、API 側で認可を行います)。 - パスが衝突するとビルドは失敗します。 2つのパッケージ(またはプラグインと組み込みページ)が同じパスを宣言すると、両方のパッケージ名とファイルを列挙した
Dashboard route collision. Route "/_authenticated/$storeId/brands/" is declared by more than one package: …が出力されます。どちらかの名前を変更してください。同じ URL のランタイムroutes:エントリーとファイルルートの衝突は検出されません。ファイルルートがそのまま優先されます。 - 実行時に条件付きで登録する必要があるページには、
routes:レジストリエントリーを引き続き使用してください。
package.json の要点
{
"name": "@acme/reviews-dashboard",
"version": "0.1.0",
"type": "module",
"sideEffects": ["./src/index.tsx", "./dist/index.js", "./dist/index.cjs"],
"spree": { "dashboard": { "plugin": true, "routes": "./src/routes" } },
"peerDependencies": {
"@spree/dashboard-core": ">=1.0.0-0 <2.0.0-0",
"@spree/dashboard-ui": ">=1.0.0-0 <2.0.0-0",
"@spree/admin-sdk": ">=1.0.0-0 <2.0.0-0",
"@tanstack/react-query": "^5",
"@tanstack/react-router": "^1",
"i18next": "^26",
"react-i18next": "^17",
"lucide-react": "^1",
"react": "^19",
"react-dom": "^19"
}
}
- React、ダッシュボードパッケージ、TanStack、i18next、lucide には、dependencies ではなく peerDependencies を使用します。 レジストリはモジュールシングルトンです。
@spree/dashboard-coreが二重に読み込まれると、登録がホストから参照されないレジストリに追加されるため、プラグインが「何もしない」状態になります。dependenciesは、ホストにない小規模なユーティリティに限って使用します。@base-ui/react(または@spree/dashboard-uiの別の依存関係)を独自のバージョンで追加しないでください。ホストではバージョンが厳密に固定されており、Base UI が二重に読み込まれるとビルドが壊れることがあります。インポートはダッシュボードパッケージのエントリーポイントからのみ行ってください。ファイルを直接指定したインポートでは、インストール済みアプリ内で Vite が CommonJS 依存関係の変換をスキップし、ダッシュボードがレンダリング前に停止することがあります。 - バージョン範囲を使用します。
spree plugin newは、@spree/cliと同時にリリースされた@spree/admin-sdk/@spree/dashboard-core/@spree/dashboard-uiのバージョンに対し、キャレット範囲を設定したひな型を生成します(例:^1.0.0-beta.3)。プレリリースに対するキャレット範囲は狭い範囲です。^1.0.0-beta.3は、それ以降の1.0.0ベータ版と安定版の1.xは受け入れますが、別バージョンのベータ版は受け入れません。そのため、1.0 がリリースされるまではダッシュボードと peerDependencies のバージョンを揃えるか、前述のように>=1.0.0-0 <2.0.0-0に範囲を広げてください。 sideEffectsにはエントリーファイルを指定する必要があります。指定しないと、バンドラーがimport '@acme/reviews-dashboard'をツリーシェイクで削除することがあります。ひな型では./src/index.tsxを指定しています。ビルド済みの JS を配布する場合は、dist/のエントリーも追加してください。- マーカー
spree.dashboard.plugin: trueは、自動検出と Tailwind のスキャンで参照されます。これがないと、ホストはプラグインを有効化できず、プラグインのクラスもコンパイルされません。 - ひな型では
"private": trueに設定されています。公開前にこれを変更し、publishConfig.access: restrictedがあれば削除してください。
ビルド済みファイルとソースのどちらを配布するか
| ソースを配布(ひな型のデフォルト) | ビルド済み JS を配布 | |
|---|---|---|
package.json | "main"/"types": "./src/index.tsx"、"files": ["src"] | "main": "./dist/index.cjs"、"module": "./dist/index.js"、"types": "./dist/index.d.ts"、"files": ["dist", "src"] |
| ビルド | tsc --noEmit(型チェックのみ)— TSX はホストの Vite がコンパイルします | tsup src/index.tsx --format cjs,esm --dts --external react + "prepublishOnly": "pnpm build" |
| 用途 | 内部パッケージ、Vite を使用するホスト(Spree のダッシュボードはすべて Vite を使用) | npm での一般公開 |
どちらの場合も、src/ は files に含めてください。Vite プラグインはパッケージを解決し、Tailwind クラスを求めてソースをスキャンします。また、ファイルルートは src/routes からコンパイルされます。静的なクラス文字列を使用してください。Tailwind は `text-${tone}-500` を認識できません。
ホストでのインストール方法(README に記載してください)
pnpm add @acme/reviews-dashboard # in apps/dashboard, then restart the dev server
# backend half, in server/
bundle add spree_reviews && bin/rails g spree_reviews:install && bin/rails db:migrate
ホストのコードを編集する必要はありません。@spree/dashboard/vite の spreeDashboardPlugin() は、ホストの dependencies と devDependencies を走査して各マニフェストを読み込み、マーカーのあるパッケージごとに次の処理を行います。
- プラグインを有効化します —
virtual:spree-dashboard-pluginsを生成します(プラグインごとに副作用インポートを 1 つ含みます)。これは、スターターのmain.tsxから、シェルの後、./pluginsの前にインポートされます。 - パッケージ用の Tailwind
@sourceを挿入します。 - ファイルルートを
routeTree.gen.tsに合成します(ルートの衝突チェックも行います)。
制御したいホストでは、代わりに許可リストを使います。これにより自動検出が無効になります。
// apps/dashboard/vite.config.ts
plugins: [spreeDashboardPlugin({ plugins: ['@acme/reviews-dashboard'] }), react()]
// plugins: [] → no third-party plugins at all
許可リストに登録されたプラグイン、または検出されたプラグインを解決できない場合は、パッケージ名とチェックリストを含む Vite のエラーオーバーレイが表示されます。プラグインをインストールすると、そのコードは管理画面内で実行されます。これは gem を追加する場合と同じ信頼モデルです。
設定可能なプラグイン
- モジュールレベルの
configure()— レンダリング時またはフェッチ時に遅延して読み込む値にのみ使用します。ES のインポートは巻き上げられるため、プラグインのトップレベルにあるdefineDashboardPluginは、ホストのconfigure(...)行より必ず先に実行されます。 definePlugin(config)ファクトリー — 設定によって登録される内容(ナビゲーション項目やルートなど)が変わる場合に使用します。インポート時にdefineDashboardPluginを呼び出さないでください。ファクトリーをエクスポートし、自動有効化するspree.dashboard.pluginエントリーを削除します(または、マーカーを Tailwind とルートの検出用に残しつつ、エントリーの副作用をなくします)。そのうえで、ホストがmain.tsx/plugins.tsからファクトリーを呼び出すよう、ドキュメントに記載してください。export function definePlugin(config: { showRatingsColumn?: boolean }) { defineDashboardPlugin({ nav: [/* … */], tables: config.showRatingsColumn ? { products: { add: [/* … */] } } : undefined }) }
Rails gem 側
ほとんどのプラグインにはエンドポイントが必要です。通常の Spree 拡張 gem を作成します(spree-extensions を参照。gem install spree_extension -v '>= 2.0' && spree-extension create reviews — 1.x では Spree 5 のスキャフォールドが生成されます)。gem の config/initializers/spree.rb でスコープ、フック、サブスクライバーを登録します。
- モデルとマイグレーション(
Spree.base_class、has_prefix_id、外部キーなし)、Spree::Api::V3::Admin配下の Admin API コントローラーとscoped_resource :reviews、シリアライザー、Spree::Core::Engine.add_routes { namespace :api … namespace :v3 … namespace :admin { resources :reviews } }によるルート — 将来コアに追加されるリソースと衝突しないよう、拡張機能の名前空間(/api/v3/admin/acme/…)を検討してください。 - ロールに権限を付与でき、ダッシュボードで
permissions.can('read', 'Spree::Review')が解決されるようにします。Spree.permissions.register_scope(:reviews, group: :catalog, resources: -> { [Spree::Review] })を登録し、gem のロケールにspree.permissions_catalog.resources.reviews.labelを追加します。 - コアモデルへの新しいカラム:
Spree::Product.additional_permitted_attributes += [:reviews_enabled]とシリアライザーの差し替え。 - 型名:gem がプロモーションルール/アクション、計算機、価格ルール、配送方法ルール、コミッションルール、販売者要件、インテグレーション、または権限を登録する場合、npm パッケージのロケールに
admin.types.<family>.<code>.{name,description}を含めます。 - gem と npm パッケージは同時にバージョンを更新します。README の互換性表には両方(
@spree/dashboard-core、spreegem)を記載してください。
公開とバージョン管理
pnpm build && pnpm publish --access public # or --tag next for pre-1.0 lines
- 配布先:公開 npm、プライベート npm、GitHub Packages(
.npmrcに@acme:registry=https://npm.pkg.github.comを設定)、または開発中はpnpm add github:acme/reviews#main。 - Changesets(任意。スキャフォールドでは設定されません):
pnpm changeset→pnpm changeset version→pnpm install→pnpm publish -r。 - CI:サポート対象の最も古いダッシュボードバージョンと次のマイナーバージョンでビルド/型チェックを実行し、依存する API がすべてのバージョンに存在するとは限らないことを検出します。
- 公開 API として扱います(破壊的変更にはメジャーバージョンの更新と変更履歴の記載が必要です):ナビゲーション/ルートのキーとパス、公開するスロット ID、ロケールキー、テーブルキー、
configure/definePluginのシグネチャ、ピア依存関係の範囲。
よくある間違い
- プラグインから
@spree/dashboardをインポートする → シェルが重複し、バージョンの固定が必要になります。 - ダッシュボードのパッケージを
dependenciesに含める → レジストリが重複し、プラグインが何もせずに動作しなくなります。 - マーカーがない、または tarball に
src/がない → プラグインが有効化されない/スタイルが適用されません。 sideEffectsがない → 本番ビルドでのみエントリーがツリーシェイクされます。- パッケージ化されたプラグインで
routes:を使ってページを登録する → 型付けされないリンクになり、同じ URL のファイルルートがあればそちらに隠されます。 - 名前空間のないキーを使う → 別のプラグインを実行しているストアで、起動時にキーの重複エラーが発生します。
spree plugin newが Rails エンジンを生成すると期待する → ダッシュボード側しかスキャフォールドされず、gem がエンドポイントを提供するまではサンプルページで 404 が発生します。- インストール/アップグレード後に開発サーバーを再起動し忘れる(検出とルートの構成は起動時に実行されます)。
さらに読むには
- ドキュメント(インストール済み):
node_modules/@spree/docs/dist/developer/dashboard/plugins/{overview,scaffolding,publishing,distributing}.md、dashboard/customization/routes.md、developer/contributing/creating-an-extension.md。オンライン:https://spreecommerce.org/docs/developer/dashboard/plugins/overview - リファレンス実装:
packages/dashboard-plugin-example(Brands)と CLI テンプレートpackages/cli/templates/plugin/(https://github.com/spree/spree) - 検出/Vite プラグインのソース:
packages/dashboard-core/src/vite/{index,discover}.ts、packages/dashboard/src/vite/route-collisions.ts - 関連スキル:
spree-dashboard(すべてのレジストリ API)、spree-extensions(gem 側)、spree-auth-permissions、spree-api-v3。