mcpField
S
spree-dashboard-plugins

spree-dashboard-plugins

github.com/spree开发10104 次安装4 周前更新
代码仓库

关于 spree-dashboard-plugins

用于打包、安装和发布 Spree 仪表板插件的开发指南。内容介绍插件脚手架、注册表、文件路由、宿主发现机制,以及配套的 Rails 扩展。

主要功能

  1. 01生成仪表板插件脚手架
  2. 02注册仪表板自定义项
  3. 03组合插件文件路由
  4. 04配置宿主插件发现
  5. 05构建 Rails 扩展后端
  6. 06发布并管理 npm 包版本

适用场景

  1. 01将仪表板功能打包供多个店铺使用
  2. 02添加插件页面与导航
  3. 03将仪表板界面连接到扩展端点
  4. 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.0 beta 版和稳定版 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 会编译 TSXtsup 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,读取每个清单,并针对所有带有该标记的包:

  1. 激活插件——生成 virtual:spree-dashboard-plugins(每个插件各有一个副作用导入),脚手架的 main.tsx 会在 shell 之后、./plugins 之前导入它。
  2. 注入 Tailwind @source,指向该包。
  3. 将其文件路由合并到 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、spree gem)。

发布与版本管理

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。

评论

支持 Markdown