Loading...
Loading...
Use when packaging Spree 6 React dashboard customizations as a redistributable npm plugin (optionally paired with a Rails extension gem) that other Spree stores install — scaffolding with `npx @spree/cli plugin new`, file routes and route collisions, peerDependencies against `@spree/dashboard-core`/`-ui`, prebuilt vs source publishing, `sideEffects`, the `spree.dashboard.plugin` package.json marker, auto-discovery via `virtual:spree-dashboard-plugins` vs an explicit whitelist, Tailwind class scanning, `definePlugin(config)` factories, Changesets, and the backend gem half. Common phrasings include "publish a dashboard plugin", "npm package for the Spree admin", "spree plugin new", "share admin screens across stores", "plugin not showing up", "Dashboard route collision", "plugin styles missing", "marketplace extension with admin UI".
npx skill4agent add spree/agent-skills spree-dashboard-pluginsdefineDashboardPlugin({...})spree-dashboardapps/dashboard/src/plugins.ts@spree/dashboard-*1.0.0-beta.xnpx @spree/cli plugin new reviews \
--npm-scope @acme --ruby-name spree_reviews --module-name Reviews \
--author "Ada Lovelace" --author-email ada@example.com --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)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/api/v3/admin/<name>buildtsc --noEmit// 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
})-core-ui@spree/dashboardacme-reviewsadmin.acme_reviews.*<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.tssrc/routestsccreate-spree-appdashboard-starterpnpm add ../reviews/packages/dashboardsubject<Can>usePermissions()Dashboard route collision. Route "/_authenticated/$storeId/brands/" is declared by more than one package: …routes:routes:{
"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"
}
}@spree/dashboard-coredependencies@base-ui/react@spree/dashboard-uispree plugin new@spree/admin-sdk@spree/dashboard-core@spree/dashboard-ui@spree/cli^1.0.0-beta.3^1.0.0-beta.31.0.01.x>=1.0.0-0 <2.0.0-0sideEffectsimport '@acme/reviews-dashboard'./src/index.tsxdist/spree.dashboard.plugin: true"private": truepublishConfig.access: restricted| Ship source (scaffold default) | Ship prebuilt JS | |
|---|---|---|
| | |
| Build | | |
| When | Internal packages, Vite hosts (all Spree dashboards are Vite) | Public npm releases |
src/filessrc/routes`text-${tone}-500`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:migratespreeDashboardPlugin()@spree/dashboard/vitedependenciesdevDependenciesvirtual:spree-dashboard-pluginsmain.tsx./plugins@sourcerouteTree.gen.ts// apps/dashboard/vite.config.ts
plugins: [spreeDashboardPlugin({ plugins: ['@acme/reviews-dashboard'] }), react()]
// plugins: [] → no third-party plugins at allconfigure()defineDashboardPluginconfigure(...)definePlugin(config)defineDashboardPluginspree.dashboard.pluginmain.tsxplugins.tsexport function definePlugin(config: { showRatingsColumn?: boolean }) {
defineDashboardPlugin({ nav: [/* … */], tables: config.showRatingsColumn ? { products: { add: [/* … */] } } : undefined })
}spree-extensionsgem install spree_extension -v '>= 2.0' && spree-extension create reviewsconfig/initializers/spree.rbSpree.base_classhas_prefix_idSpree::Api::V3::Adminscoped_resource :reviewsSpree::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] })spree.permissions_catalog.resources.reviews.labelSpree::Product.additional_permitted_attributes += [:reviews_enabled]admin.types.<family>.<code>.{name,description}@spree/dashboard-corespreepnpm build && pnpm publish --access public # or --tag next for pre-1.0 lines@acme:registry=https://npm.pkg.github.com.npmrcpnpm add github:acme/reviews#mainpnpm changesetpnpm changeset versionpnpm installpnpm publish -rconfiguredefinePlugin@spree/dashboarddependenciessrc/sideEffectsroutes:spree plugin newnode_modules/@spree/docs/dist/developer/dashboard/plugins/{overview,scaffolding,publishing,distributing}.mddashboard/customization/routes.mddeveloper/contributing/creating-an-extension.mdpackages/dashboard-plugin-examplepackages/cli/templates/plugin/packages/dashboard-core/src/vite/{index,discover}.tspackages/dashboard/src/vite/route-collisions.tsspree-dashboardspree-extensionsspree-auth-permissionsspree-api-v3