关于 spree-dashboard-plugins
用于打包、安装和发布 Spree 仪表板插件的开发指南。内容介绍插件脚手架、注册表、文件路由、宿主发现机制,以及配套的 Rails 扩展。
主要功能
- 01生成仪表板插件脚手架
- 02注册仪表板自定义项
- 03组合插件文件路由
- 04配置宿主插件发现
- 05构建 Rails 扩展后端
- 06发布并管理 npm 包版本
适用场景
- 01将仪表板功能打包供多个店铺使用
- 02添加插件页面与导航
- 03将仪表板界面连接到扩展端点
- 04准备插件并发布到 npm
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-dashboard-plugins安装后在 Claude Code 中输入/spree-dashboard-plugins确认已加载。
详细介绍
Spree Dashboard 插件 — 打包与分发
Dashboard 插件是一个 npm 包,其入口模块会在导入时调用 defineDashboardPlugin({...})。它使用的注册表与应用内自定义完全相同(nav、settingsNav、routes、slots、tables、formFields、customFieldComponents — 详见 spree-dashboard)。变化之处在于打包方式:对等依赖、发现机制、文件路由和发布。
默认情况下,不要急着使用插件。 对于自己的商店,请将代码放在 apps/dashboard/src/plugins.ts 中。只有在多个商店/项目都要安装时(开源集成、代理商部署、商业附加组件),才将其打包。之后再将应用内代码升级为插件,大多只是复制代码。
Dashboard 目前处于 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
Dashboard 包的名称为 <scope>/<name>-dashboard。CLI 只生成 Dashboard 部分——生成的 README 会明确说明这一点,并指向扩展指南。示例页面会调用 /api/v3/admin/<name> 端点,但只有在你将后端部分作为独立扩展 gem 构建后,这些端点才会存在(见下文)。脚手架生成的包使用 build 脚本 tsc --noEmit:它会对包中包含的 TypeScript 源代码进行类型检查,但不会输出 JS(见下文“预构建版本与源码”)。
入口模块
// 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/dashboard:插件接入的是宿主的界面外壳;导入外壳包会将整个应用带入你的包,并将其绑定到某个 Dashboard 版本。
所有内容都使用命名空间——注册表键、槽位 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——升级插件后,重启开发服务器即可添加其路由。 - 将
src/routes排除在包的独立tsc检查之外(脚手架会这样处理)——路由文件只能在宿主程序中进行类型检查。请在宿主项目中开发(create-spree-app项目或dashboard-starter检出版本),并链接你的包(pnpm add ../reviews/packages/dashboard或通过 workspace)。 - 文件路由会跳过注册表分发器,因此
subject门控不适用——请在组件内部使用<Can>/usePermissions()进行门控(仅影响 UX;API 会进行授权)。 - 路由冲突会导致构建失败:两个包(或一个插件与一个内置页面)声明相同路径时,会产生
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"
}
}
- **使用
peerDependencies,而非dependencies,**来声明 React、仪表板包、TanStack、i18next 和 lucide。注册表是模块单例:如果有第二份@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.0beta 版和稳定版1.x,但不会接受其他版本的 beta 版——因此,在 1.0 发布前,请让 peer 依赖与仪表板保持同步,或像上文那样放宽到>=1.0.0-0 <2.0.0-0。 sideEffects必须列出入口文件,否则打包工具可能会把import '@acme/reviews-dashboard'tree-shake 掉。脚手架会列出./src/index.tsx;如果要发布预构建 JS,也请添加dist/条目。- 标记
spree.dashboard.plugin: true是自动发现和 Tailwind 扫描所需的内容。没有它,宿主就不会激活插件,其类也无法编译。 - 脚手架设置了
"private": true——发布前请将其改为false(并删除任何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(仅进行类型检查)——宿主的 Vite 会编译 TSX | 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
无需修改宿主代码。spreeDashboardPlugin()(来自 @spree/dashboard/vite)会遍历宿主的 dependencies + devDependencies,读取每个清单,并针对所有带有该标记的包:
- 激活插件——生成
virtual:spree-dashboard-plugins(每个插件各有一个副作用导入),脚手架的main.tsx会在 shell 之后、./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下并使用scoped_resource :reviews的 Admin API 控制器、序列化器,以及通过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 的 locale 中添加spree.permissions_catalog.resources.reviews.label。 - 在核心模型上添加新列:
Spree::Product.additional_permitted_attributes += [:reviews_enabled],并替换序列化器。 - 类型名称:如果 gem 注册了促销规则/操作、计算器、价格规则、配送方式规则、佣金规则、卖家要求、集成或权限,请在 npm 包的 locales 中提供
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、locale 键、表格键、
configure/definePlugin签名、peer 依赖范围。
常见错误
- 在插件中从
@spree/dashboard导入 → 重复的外壳、版本锁定。 - 将仪表板包放在
dependencies中 → 注册表重复;插件会静默失效。 - 缺少标记或 tarball 中没有
src/→ 插件未激活/没有样式。 - 缺少
sideEffects→ 仅在生产构建中入口会被 tree-shaking 移除。 - 在已打包的插件中使用
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)以及 https://github.com/spree/spree 中的 CLI 模板packages/cli/templates/plugin/ - 发现机制/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。