@spree/docs 0.1.122 → 0.1.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/api-reference/admin-api/endpoints.md +1 -1
  2. package/dist/developer/cli/admin-api.md +11 -0
  3. package/dist/developer/dashboard/concepts.md +103 -0
  4. package/dist/developer/dashboard/customization/backend.md +126 -0
  5. package/dist/developer/dashboard/customization/navigation.md +211 -0
  6. package/dist/developer/dashboard/customization/permissions.md +123 -0
  7. package/dist/developer/dashboard/customization/quickstart.md +100 -0
  8. package/dist/developer/dashboard/customization/routes.md +187 -0
  9. package/dist/developer/dashboard/customization/slots.md +114 -0
  10. package/dist/developer/dashboard/customization/tables.md +167 -0
  11. package/dist/developer/dashboard/customization/translations.md +96 -0
  12. package/dist/developer/dashboard/deployment.md +80 -0
  13. package/dist/developer/dashboard/overview.md +105 -0
  14. package/dist/developer/dashboard/plugins/distributing.md +137 -0
  15. package/dist/developer/dashboard/plugins/overview.md +46 -0
  16. package/dist/developer/dashboard/plugins/publishing.md +142 -0
  17. package/dist/developer/dashboard/plugins/scaffolding.md +133 -0
  18. package/dist/developer/dashboard/public-api.md +241 -0
  19. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +212 -0
  20. package/dist/developer/dashboard/recipes/custom-form-field.md +174 -0
  21. package/dist/developer/dashboard/recipes/page-action-button.md +189 -0
  22. package/dist/developer/dashboard/recipes/sidebar-widget.md +147 -0
  23. package/dist/developer/dashboard/slots-catalog.md +173 -0
  24. package/dist/developer/sdk/admin/extending.md +70 -0
  25. package/dist/developer/sdk/admin/quickstart.md +3 -3
  26. package/dist/developer/tutorial/admin.md +2 -0
  27. package/dist/developer/tutorial/api.md +2 -0
  28. package/dist/developer/tutorial/extending-models.md +2 -0
  29. package/dist/developer/tutorial/model.md +2 -0
  30. package/package.json +1 -1
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: Deploying the dashboard
3
+ sidebarTitle: Deployment
4
+ description: Ship the React Dashboard to production — served by the Spree server itself (the default), or as a static build on a CDN.
5
+ ---
6
+
7
+ The dashboard deploys as **static files**. `vite build` produces `dist/` — HTML, JS, CSS — there is no Node server in production. What varies is who serves those files.
8
+
9
+ > **WARNING:** `VITE_`-prefixed values are compiled into the client bundle — never put credentials in them. The dashboard authenticates admins interactively (JWT in memory + an httpOnly refresh cookie); no API keys are involved.
10
+
11
+ ## Single node — the default
12
+
13
+ The Spree server serves the dashboard itself at **`/dashboard`**: one container, one origin, zero CORS or cookie configuration. The official Spree Docker image ships with the stock dashboard baked in — deploy the image anywhere Docker runs (Render, Railway, AWS, Azure, your own host) and `https://your-api-host/dashboard` just works.
14
+
15
+ Under the hood, the **`spree_dashboard` gem** (in the Gemfile of the starter and the official image) serves the directory in `Spree::Dashboard.dist_path` — or the `SPREE_DASHBOARD_DIST_PATH` env var — with SPA semantics: every dashboard route falls back to `index.html`, hashed assets get immutable caching. The bundle is built with a **relative API base** (`VITE_SPREE_API_URL` unset) and `base: /dashboard/`, so the same image works on every host and environment with no rebuild. API-only deployments simply omit the gem.
16
+
17
+ ### Custom dashboard, single node
18
+
19
+ The official image contains the *stock* dashboard. Once you've customized yours (your own pages/plugins in `apps/dashboard/`), bake your build into your own image:
20
+
21
+ ```bash
22
+ spree build --production # optionally --tag registry/repo:tag
23
+ ```
24
+
25
+ The command detects `apps/dashboard/`, selects the Dockerfile's `custom` dashboard stage (`--build-arg DASHBOARD_SOURCE=custom`), and passes your app as a named build context — the Node toolchain lives only in that throw-away stage, and the final image is Rails plus your built `dist/` served at `/dashboard`. Without the CLI, the equivalent is:
26
+
27
+ ```bash
28
+ docker build backend/ -f backend/Dockerfile \
29
+ --build-arg DASHBOARD_SOURCE=custom \
30
+ --build-context dashboard-src=./apps/dashboard \
31
+ -t my-shop-spree:latest
32
+ ```
33
+
34
+ Inside the stage the build runs with `VITE_BASE_PATH=/dashboard/` (asset URLs resolve under the mount) and `VITE_SPREE_API_URL` unset (API calls stay origin-relative). Projects **without** `apps/dashboard/` build exactly what plain `docker build backend/` produces — and if your `backend/Dockerfile` predates dashboard support, the command warns instead of silently shipping the stock dashboard over your customized one.
35
+
36
+ ## Static host / CDN — the alternative
37
+
38
+ For edge-served assets or an independent dashboard release cadence, deploy `dist/` to a static host on its own domain:
39
+
40
+ ```bash
41
+ cd apps/dashboard
42
+ VITE_SPREE_API_URL=https://api.mystore.com pnpm build
43
+ ```
44
+
45
+ This is the **cross-origin** topology, with three requirements:
46
+
47
+ 1. **HTTPS on both sides.** The refresh cookie is issued with `SameSite=None; Secure` in production, and browsers drop `Secure` cookies over HTTP. There is no insecure-production mode.
48
+ 2. **Allowed Origins.** Add the dashboard's URL (e.g. `https://dashboard.mystore.com`) to **Settings → Allowed Origins** in the Spree admin — the API rejects cross-origin requests until then.
49
+ 3. **SPA fallback.** Every path must rewrite to `index.html` so the router owns the URL (`/* → /index.html` on your host: `_redirects` on Netlify, `vercel.json` rewrites, `try_files $uri /index.html;` on nginx).
50
+
51
+ A middle ground also works: serve `dist/` and proxy `/api/*` + `/rails/*` to Rails from one domain behind your own nginx/Caddy/ingress — same-origin simplicity without baking the bundle into the image.
52
+
53
+ ## Render
54
+
55
+ If your project deploys to Render via the Blueprint that `create-spree-app` places at the project root, `spree add dashboard` wires the single-node topology automatically: it extends the backend service's `buildCommand` to also build `apps/dashboard` (Render's Ruby runtime includes Node, and the full repo is available during the build) and adds one env var so the server serves the result:
56
+
57
+ ```yaml
58
+ buildCommand: bundle install && … && (cd ../apps/dashboard && corepack enable pnpm && pnpm install && VITE_BASE_PATH=/dashboard/ pnpm build)
59
+ envVars:
60
+ - key: SPREE_DASHBOARD_DIST_PATH
61
+ value: ../apps/dashboard/dist
62
+ ```
63
+
64
+ That's the whole setup — one service, one origin, **your customized dashboard** (it builds from your `apps/dashboard`, not the stock bundle) at `https://your-service.onrender.com/dashboard`. No `VITE_SPREE_API_URL`, no Allowed Origins entry, no extra service. Redeploys rebuild on every push.
65
+
66
+ If you prefer the dashboard on Render's CDN instead, add a static-site service by hand using the cross-origin recipe above.
67
+
68
+ ## Any other static host
69
+
70
+ The same three ingredients, by hand: build with `VITE_SPREE_API_URL` set, upload `dist/`, configure the `/* → /index.html` rewrite. Then add the origin to Allowed Origins.
71
+
72
+ ## Checklist (cross-origin only)
73
+
74
+ Single-node deployments skip all of this — one origin needs none of it.
75
+
76
+ - [ ] `VITE_SPREE_API_URL` points at the production API (baked at build time — one build per environment)
77
+ - [ ] HTTPS on the dashboard *and* the API
78
+ - [ ] Dashboard origin added to **Settings → Allowed Origins**
79
+ - [ ] All paths rewrite to `index.html`
80
+ - [ ] No secrets in any `VITE_` variable
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: React Dashboard Overview
3
+ sidebarTitle: Overview
4
+ description: Learn about the Spree React Dashboard architecture and how to customize it and extend it
5
+ ---
6
+
7
+ > **INFO:** The React Dashboard is currently a **Developer Preview** — APIs may still change between releases.
8
+ > In Spree 6 it will become the default Admin interface replacing the Spree 5 admin.
9
+
10
+ The React Dashboard is a React single-page application (SPA) that talks to the [Spree Admin API](../../api-reference/admin-api.md). It can be distributed with the Spree API or as a standalone npm package, and it can be extended with plugins. The dashboard is built with modern React libraries and patterns, including:
11
+
12
+ | Layer | Choice |
13
+ |---|---|
14
+ | Build & dev | [Vite](https://vitejs.dev/) |
15
+ | Routing | [TanStack Router](https://tanstack.com/router) (file-based, type-safe) |
16
+ | Data fetching | [TanStack Query](https://tanstack.com/query) |
17
+ | Forms | [React Hook Form](https://react-hook-form.com/) + [Zod](https://zod.dev/) |
18
+ | UI primitives | [shadcn/ui](https://ui.shadcn.com/) + [Base UI](https://base-ui.com/) + [Tailwind CSS](https://tailwindcss.com/) |
19
+ | Icons | [lucide-react](https://lucide.dev/) |
20
+ | Charts | [Recharts](https://recharts.org/) |
21
+ | Rich text | [Tiptap](https://tiptap.dev/) |
22
+ | Notifications | [Sonner](https://sonner.emilkowal.ski/) |
23
+ | Lint & format | [Biome](https://biomejs.dev/) |
24
+
25
+ ### Packages
26
+
27
+ Dashboard consinsts of 3 NPM packages, each with a different purpose. You can import any of them in your host app or plugin package.
28
+
29
+ | Package | What it contains | When you touch it |
30
+ |---|---|---|
31
+ | `@spree/dashboard-ui` | Design system: [shadcn](https://ui.shadcn.com/) primitives + headless composed components (`PageHeader`, `ResourceLayout`, `ResourceTable`'s rendering layer, etc.) + design tokens | Building custom UI — primitives + compounds you can drop anywhere |
32
+ | `@spree/dashboard-core` | Framework: registries (nav, route, slot, table, settings-nav), providers (auth, permission, store), admin SDK client, infra hooks, the `defineDashboardPlugin` API | Extending the admin — every customization API lives here |
33
+ | `@spree/dashboard` | The deployable SPA: routes, resource hooks, schemas, locale strings, app shell | Replacing or restyling specific routes (rare) |
34
+
35
+ Most customization happens via `@spree/dashboard-core`. You import its registries from your own host-app code (or from a separate plugin package) to add nav entries, routes, slot widgets, table columns, and translations.
36
+
37
+ You can also built your own dashboard using `@spree/dashboard-ui` primitives and `@spree/dashboard-core` registries, if you want to replace the default dashboard entirely. This is rare — most teams just add a few pages or cards to the existing dashboard.
38
+
39
+ ## Get the dashboard
40
+
41
+ Your copy of the dashboard is a small Vite app (the "host app") that imports `<Dashboard />` from `@spree/dashboard` — you own its `package.json`, Vite config, and a `src/plugins.ts` for customizations. Two ways to get one:
42
+
43
+ - **New project:** `npx create-spree-app my-store` — answer **Yes** to "Include React Dashboard?" and it lands in `apps/dashboard/`, pointed at your API.
44
+ - **Existing project:** run `npx spree add dashboard` from your project root — same result.
45
+
46
+ Either way: `cd apps/dashboard && npm run dev`, open http://localhost:5173, and sign in with your admin email and password. No API keys to configure — admins authenticate interactively.
47
+
48
+ ## Choose your path
49
+
50
+ There are two ways to customize the dashboard. **Both use the same APIs** — the only difference is packaging.
51
+
52
+ ### 1. Customize in your host app (Recommended)
53
+
54
+ You're building a Spree store and want a "Featured products" page or a custom card on every product detail. Edit your dashboard app directly — it ships a `src/plugins.ts` file for exactly this (in a `create-spree-app` project the dashboard app lives at `apps/dashboard/`):
55
+
56
+ ```tsx
57
+ // src/plugins.ts — already imported by the app; just add your registrations
58
+ import { defineDashboardPlugin } from '@spree/dashboard-core'
59
+ import { MyAnalyticsPage } from './pages/analytics'
60
+
61
+ defineDashboardPlugin({
62
+ nav: [{ key: 'analytics', label: 'Analytics', path: '/analytics', position: 650 }],
63
+ routes: [{ key: 'analytics', path: '/analytics', component: MyAnalyticsPage }],
64
+ })
65
+ ```
66
+
67
+ That's the entire setup. No npm publishing, no peer dependencies, no build configuration — you're inside the app's own build, so styling and hot reload just work. Most teams stop here.
68
+
69
+ → Start at the [**customization quickstart**](customization/quickstart.md).
70
+
71
+ ### 2. Ship a redistributable plugin
72
+
73
+ You're building a feature that multiple Spree stores will install — a Brands gem, a Wishlists integration, a Stripe Tax connector. Package the customization as an npm module so any host app can install it with a single `pnpm add my-plugin`.
74
+
75
+ This adds three concerns on top of in-app customization:
76
+
77
+ - **Peer-dependency rules** so your plugin's `@spree/dashboard-core` resolves to the host's instance (registries are module singletons — see [Publishing](plugins/publishing.md))
78
+ - **Auto-discovery** — the dashboard's build finds every installed dependency carrying the `spree.dashboard.plugin` marker, activates it, and wires up its styling. Installing a plugin is `pnpm add` + a dev-server restart; nothing to edit. See [Distributing](plugins/distributing.md) for how it works and the explicit-whitelist escape hatch.
79
+ - **File routes** — a plugin ships its pages as TanStack file routes that get compiled into the app's route tree, so links to plugin pages are fully type-checked. See [Routes](customization/routes.md). (In-app customizations use the simpler `routes:` registry instead, as above.)
80
+ - **A backend half** — a Rails extension gem that ships the API endpoints your dashboard plugin calls
81
+
82
+ The `@spree/cli` scaffolds the dashboard half for you:
83
+
84
+ ```bash
85
+ npx @spree/cli plugin new my-feature
86
+ ```
87
+
88
+ → Start at the [**plugin scaffolding guide**](plugins/scaffolding.md).
89
+
90
+ ## Which one should I pick?
91
+
92
+ Use this rule of thumb:
93
+
94
+ - **Customize in-app** if the feature is store-specific (your branding, your team's workflow, your custom domain logic) or you don't yet know whether others would want it. The cost to "promote" it to a plugin later is small.
95
+ - **Ship a plugin** if you're certain others will install it (you're publishing to the marketplace, your agency is rolling it out across clients, the feature has a clear standalone identity).
96
+
97
+ When in doubt, customize in-app first. The migration path to a plugin is mechanical — same APIs, just wrapped in their own package.
98
+
99
+ ## Reference
100
+
101
+ - [Customization quickstart](customization/quickstart.md)
102
+ - [Plugin scaffolding](plugins/scaffolding.md)
103
+ - [Slots catalog](slots-catalog.md) — every named slot the dashboard exposes
104
+ - [`@spree/dashboard-core` README](https://github.com/spree/spree/tree/main/packages/dashboard-core) — full extension API reference
105
+ - [Classic Admin extension model](../admin/extending-ui.md) — for Spree 5.x
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Distributing
3
+ sidebarTitle: Distributing
4
+ description: What consumers do to install your plugin — the wiring on the host side, what to document, and how to keep upgrades painless.
5
+ ---
6
+
7
+ This page is the *consumer's* perspective on installing your plugin. Cover it in your README so users have a one-page reference.
8
+
9
+ ## The two steps
10
+
11
+ A consumer installing your plugin needs to:
12
+
13
+ 1. **Install the npm package** — `pnpm add @your-scope/your-plugin`, then restart the dev server
14
+ 2. **Apply any backend changes** — usually adding a Rails gem, running migrations
15
+
16
+ ```bash
17
+ pnpm add @your-scope/your-plugin
18
+
19
+ # Rails side, if your plugin has a backend
20
+ bundle add your_plugin
21
+ bin/rails generate your_plugin:install
22
+ bin/rails db:migrate
23
+ ```
24
+
25
+ **No host-code edit.** As long as your plugin's `package.json` declares the `spree.dashboard.plugin` marker (the CLI scaffold does this for you — see [Publishing](publishing.md#tell-the-vite-plugin-where-to-find-you)), `spreeDashboardPlugin()` walks the host's dependencies at build time and finds it automatically. Discovery feeds two things:
26
+
27
+ - **Activation** — the app entry imports `virtual:spree-dashboard-plugins` (the dashboard app shell and the starter template already do), a module the Vite plugin synthesizes with one side-effect import per discovered plugin. Your `defineDashboardPlugin` call runs before first render without the host touching `main.tsx`.
28
+ - **Tailwind scanning** — your plugin's class names compile without any extra wiring.
29
+
30
+ Installing a package activating its code in the admin is the same trust model as adding a gem to a Rails `Gemfile` — vet your dependencies. Hosts that want explicit control use the whitelist below.
31
+
32
+ Plugins that take configuration (the `definePlugin(config)` factory pattern — see below) are the exception: the factory call is host code, so the host imports and invokes it in `main.tsx` themselves.
33
+
34
+ ### Manual whitelist (advanced)
35
+
36
+ For tightly controlled environments (a host that wants explicit allowlist semantics, or a plugin you want to load conditionally), pass the names explicitly:
37
+
38
+ ```ts
39
+ // vite.config.ts (your dashboard app)
40
+ spreeDashboardPlugin({
41
+ plugins: ['@your-scope/your-plugin'], // disables auto-discovery
42
+ })
43
+ ```
44
+
45
+ Passing `plugins: []` disables auto-discovery entirely — useful if you want to ship a vanilla dashboard with zero third-party Tailwind sources.
46
+
47
+ ## Dev-mode banner
48
+
49
+ If the consumer registers your plugin explicitly but it's not installed (or the spelling is off), `@spree/dashboard-core/vite` shows a Vite error overlay during dev with the package name and a fix-it checklist. The same overlay fires if a discovered plugin can't be resolved on disk. That replaces the older silent-failure mode where missing plugins emitted unstyled HTML.
50
+
51
+ ## What to put in your README
52
+
53
+ A README that gets users to "working in five minutes" has:
54
+
55
+ - A one-paragraph "what this does"
56
+ - Screenshots or a short clip of the dashboard UI it adds
57
+ - The four install steps (literally copy-paste blocks)
58
+ - A "configuration" section if the plugin has runtime config
59
+ - A "compatibility" table (`@spree/dashboard >=6.0.0`, `Spree >=6.0.0`, etc.)
60
+ - Links to the source for slots/components the plugin overrides
61
+
62
+ Skip the "philosophy" and the "alternative approaches" sections. Get users to working in minutes.
63
+
64
+ ## Configuration patterns
65
+
66
+ Plugins occasionally need runtime config — API endpoints, feature toggles, custom permissions. Two patterns:
67
+
68
+ ### Module-level `configure` call
69
+
70
+ ```ts
71
+ // in your plugin
72
+ let _config = { endpoint: '/default' }
73
+ export function configure(opts: { endpoint?: string }) {
74
+ _config = { ..._config, ...opts }
75
+ }
76
+ export function getConfig() { return _config }
77
+ ```
78
+
79
+ ```ts
80
+ // consumer
81
+ import { configure } from '@your-scope/your-plugin'
82
+ configure({ endpoint: '/custom' })
83
+ ```
84
+
85
+ Simple, no dependencies — but mind the timing: ES module imports are hoisted, so the plugin module (including any top-level `defineDashboardPlugin` call) always executes **before** your `configure(...)` line runs. This pattern therefore only works for values the plugin reads lazily — at render or fetch time, like an API endpoint — never for anything its import-time registration depends on. If config shapes what gets registered (which nav entries, which routes), use the factory pattern below instead.
86
+
87
+ ### `definePlugin(config)` factory
88
+
89
+ ```ts
90
+ // in your plugin
91
+ export function definePlugin(config: PluginConfig) {
92
+ // call defineDashboardPlugin inside, closing over config
93
+ }
94
+ ```
95
+
96
+ ```ts
97
+ // consumer
98
+ import { definePlugin } from '@your-scope/your-plugin'
99
+ definePlugin({ endpoint: '/custom' })
100
+ ```
101
+
102
+ A bit more boilerplate but easier to test, and the order is unambiguous — no side-effect-at-import-time. If you go this route, drop the auto-registering entry-point and document `definePlugin` as *the* way to install.
103
+
104
+ ## Upgrades
105
+
106
+ When you cut a major version of your plugin, the breaking-changes section of your changelog needs to call out:
107
+
108
+ - Renamed registry keys (`nav.add({ key: ... })` changes are user-visible — their URLs depend on it)
109
+ - Locale-key renames (consumers may have overrides keyed off old names)
110
+ - Slot ID changes (consumers extending your slots break)
111
+ - `peerDependencies` range bumps (consumers may need to upgrade Spree first)
112
+
113
+ Treat the plugin's public surface as: nav keys, route paths, slot IDs, locale keys, and the exported `configure` / `definePlugin` API. Everything else is internal.
114
+
115
+ ## Coexistence with other plugins
116
+
117
+ The registries are global singletons, so a key collision is the consumer's problem to debug. Mitigations:
118
+
119
+ - **Namespace your keys** — `key: 'acme-brands'` not `key: 'brands'`.
120
+ - **Namespace locale keys** — `admin.acme_brands.title` not `admin.brands.title`.
121
+ - **Namespace API paths** — your Rails engine should mount under `/api/v3/admin/acme/...`, not `/api/v3/admin/brands` (which collides with core Brands if it ever ships).
122
+
123
+ The dashboard logs a warning in dev mode when two registry entries share a key. That's the consumer's signal that two plugins are stepping on each other — and it falls back to "last-registered wins", which is rarely what either plugin wanted.
124
+
125
+ ## Distribution channels
126
+
127
+ - **npm public** — what most plugins want. `pnpm publish --access public`.
128
+ - **npm private (paid)** — for commercial plugins; combine with a license key checked at runtime.
129
+ - **GitHub Package Registry** — internal-only; consumers add `@your-scope:registry=https://npm.pkg.github.com` to `.npmrc`.
130
+ - **Git tarball** — `pnpm add github:your-org/your-plugin#main` works for unpublished plugins, useful while iterating against a single consumer.
131
+
132
+ For pre-1.0 plugins, ship under the `next` dist-tag (`pnpm publish --tag next`) so `pnpm add @your-scope/your-plugin` resolves to the stable line and `pnpm add @your-scope/your-plugin@next` opts in.
133
+
134
+ ## Reference
135
+
136
+ - [Example consumer wiring](https://github.com/spree/spree/blob/main/packages/dashboard/vite.config.ts) — what `spreeDashboardPlugin` looks like in production
137
+ - [Plugins reference (`brands` example)](https://github.com/spree/spree/tree/main/packages/dashboard-plugin-example) — what a finished plugin looks like end-to-end
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: Plugin overview
3
+ sidebarTitle: Overview
4
+ description: Package dashboard customizations as a redistributable npm plugin. Read this if you're shipping a feature for *other* people's Spree projects — not your own.
5
+ ---
6
+
7
+ If you're customizing the dashboard for your own project, you don't need a plugin. Drop the same code into `src/plugins.ts` in your dashboard app and skip this section entirely — start with the [Customization](../customization/quickstart.md) docs.
8
+
9
+ This section is for the ~10% of cases where you want to **distribute** dashboard extensions: as an open-source integration, an internal package shared across multiple host apps, or a SaaS-style commercial add-on.
10
+
11
+ ## What "plugin" means here
12
+
13
+ A dashboard plugin is:
14
+
15
+ 1. **An npm package** — published, versioned, installed via `pnpm add @your-scope/your-plugin`.
16
+ 2. **One side-effecting entry-point** — `import '@your-scope/your-plugin'` calls `defineDashboardPlugin({ ... })` at module-load time, which registers nav entries, routes, slots, columns, etc.
17
+ 3. **Auto-discovered** — the `"spree": { "dashboard": { "plugin": true } }` marker in your package.json is all the host needs; `spreeDashboardPlugin()` finds every installed dependency carrying it, activates the entry-point, and scans your source for Tailwind classes so you never ship pre-built CSS. (Hosts can pass an explicit `plugins: [...]` array to opt out of discovery and whitelist instead.)
18
+ 4. **Translations-aware** — your bundle ships JSON locale files and calls `i18n.addResourceBundle('en', 'translation', bundle, true, true)` on import (keys nested under a top-level `admin` object).
19
+ 5. **Possibly Rails-aware** — many plugins also ship a Rails engine alongside the dashboard package (new models, new API endpoints) so the dashboard has something to talk to.
20
+
21
+ That's it. **Plugins use the exact same registries as in-app customizations.** Everything you can do in `src/plugins.ts` you can also do from inside a plugin entry-point. The split between "in-app" and "plugin" is about packaging and distribution, not capability.
22
+
23
+ ## When to choose plugin over in-app
24
+
25
+ | Scenario | Approach |
26
+ |---|---|
27
+ | Single project, your own team | In-app customization |
28
+ | Two-three internal projects sharing the same extension | Internal npm package (still a plugin) |
29
+ | Open-source integration distributed via npm | Plugin |
30
+ | Commercial add-on with versioning + changelog | Plugin |
31
+ | One-off prototype, even at a client | In-app (you can extract later) |
32
+
33
+ If you're not sure: start in-app. Lifting `src/plugins.ts` into a plugin package later is a half-hour refactor — moving in the other direction is more painful.
34
+
35
+ ## What's in this section
36
+
37
+ - [Scaffolding](scaffolding.md) — `spree plugin new <name>` generates the monorepo
38
+ - [Publishing](publishing.md) — package metadata, semver, the npm bits
39
+ - [Distributing](distributing.md) — what consumers do to install it
40
+
41
+ If you haven't yet, read the [Customization](../customization/quickstart.md) section first. It's where the registry API is documented; the plugin pages mostly cover *packaging* concerns.
42
+
43
+ ## Reference
44
+
45
+ - [Reference: Plugin example (`brands` repo)](https://github.com/spree/spree/tree/main/packages/dashboard-plugin-example) — vendored reference implementation
46
+ - [`defineDashboardPlugin` source](https://github.com/spree/spree/blob/main/packages/dashboard-core/src/plugin.ts)
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Publishing
3
+ sidebarTitle: Publishing
4
+ description: Get a dashboard plugin onto npm — package metadata, peer dependencies, TypeScript builds, versioning with Changesets.
5
+ ---
6
+
7
+ `@spree/cli plugin new` scaffolds the npm package shape, but you still need to fill in publishing-specific metadata before `pnpm publish`. This page covers what to set and why.
8
+
9
+ ## Set the right `peerDependencies`
10
+
11
+ Your plugin imports React, the dashboard, Tailwind, i18next, etc., but **you don't bundle them** — the host app has those, and bundling them twice causes either duplicate module instances or a tree-shaken nightmare. Use `peerDependencies`:
12
+
13
+ ```json
14
+ {
15
+ "peerDependencies": {
16
+ "react": "^19",
17
+ "react-dom": "^19",
18
+ "@spree/dashboard-core": "^0.1.0",
19
+ "@spree/dashboard-ui": "^0.1.0",
20
+ "@spree/admin-sdk": "^0.5.0",
21
+ "i18next": "^26",
22
+ "react-i18next": "^17",
23
+ "@tanstack/react-query": "^5",
24
+ "@tanstack/react-router": "^1",
25
+ "lucide-react": "^1"
26
+ }
27
+ }
28
+ ```
29
+
30
+ Use ranges, not exact versions — pin too tightly and consumers can't upgrade the dashboard without you cutting a release. One Developer Preview caveat: for `0.x` versions, `^` only allows patch-level drift (`^0.1.0` matches `0.1.x`, not `0.2.0`), so expect to bump your `@spree/dashboard-*` peers alongside dashboard releases until 1.0. `spree plugin new` scaffolds these ranges for you, matched to the current release.
31
+
32
+ `dependencies` is for things your plugin *uses* that the host *doesn't* — small utilities (e.g., `clsx`, `date-fns` if you need a specific version, your own helper packages). When in doubt, peer-dep it. The trade-off is "user has to install one more thing" vs. "user has two copies of React" — always pay the first cost.
33
+
34
+ ## Choose what to publish
35
+
36
+ The scaffold's `package.json` ships TS source by default. Two options:
37
+
38
+ ### Option A — Ship pre-built JS
39
+
40
+ Run `tsup` on publish, ship `dist/`:
41
+
42
+ ```json
43
+ {
44
+ "main": "./dist/index.cjs",
45
+ "module": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "files": ["dist", "src/locales"],
48
+ "scripts": {
49
+ "build": "tsup src/index.tsx --format cjs,esm --dts --external react",
50
+ "prepublishOnly": "pnpm build"
51
+ }
52
+ }
53
+ ```
54
+
55
+ Pre-built JS is friendlier to consumers using non-Vite bundlers and means your code runs regardless of the host's TS config.
56
+
57
+ ### Option B — Ship source
58
+
59
+ ```json
60
+ {
61
+ "main": "./src/index.tsx",
62
+ "types": "./src/index.tsx",
63
+ "files": ["src"]
64
+ }
65
+ ```
66
+
67
+ Vite + tsup can consume TSX directly. Simpler builds, hot reload across the boundary in dev, but only works for Vite-host consumers. Spree dashboards always use Vite, so this is safe in practice.
68
+
69
+ Pick A if you're going to npm in earnest. Pick B for internal-only packages used inside a known-Vite setup.
70
+
71
+ ## Side-effects flag
72
+
73
+ The plugin's whole purpose is side effects (`defineDashboardPlugin` registers stuff at module-load time). **Tell bundlers not to tree-shake it:**
74
+
75
+ ```json
76
+ {
77
+ "sideEffects": ["./src/index.tsx", "./dist/index.js", "./dist/index.cjs"]
78
+ }
79
+ ```
80
+
81
+ Without this, a bundler can decide that `import '@your-scope/your-plugin'` does nothing useful and drop it. The list is what's allowed to have effects; everything else still tree-shakes.
82
+
83
+ ## Tell the Vite plugin where to find you
84
+
85
+ Declare your package as a dashboard plugin in its own `package.json`:
86
+
87
+ ```json
88
+ {
89
+ "spree": {
90
+ "dashboard": {
91
+ "plugin": true,
92
+ "routes": "./src/routes"
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ `routes` (optional) points at your TanStack file-routes directory; the host build compiles it into its typed route tree — see the [routes guide](../customization/routes.md).
99
+
100
+ This marker is what powers **auto-discovery**: when a host calls `spreeDashboardPlugin()` without an explicit `plugins` array (the default), the Vite plugin walks the host's `package.json` deps + devDeps, reads each one's manifest, and picks up anything with `spree.dashboard.plugin: true`. The host doesn't have to know your plugin's name — `pnpm add @your-scope/your-plugin` is the entire installation step.
101
+
102
+ The CLI scaffold (`spree plugin new`) writes this field for you. If you're hand-rolling a plugin, add it before publishing — without it, hosts that rely on auto-discovery won't find your plugin and Tailwind won't pick up your class names.
103
+
104
+ ## Export the Tailwind source
105
+
106
+ `spreeDashboardPlugin` uses `require.resolve(...)` to find your package, then walks `src/` for Tailwind class scanning. As long as you ship `src/`, it works — Tailwind picks up `className="..."` strings from your built code without you publishing CSS.
107
+
108
+ If you only ship `dist/` and your built code mangles class names (Tailwind can't see `text-${variant}-500`), use static class names or expose a `tailwind.config.js` content array.
109
+
110
+ ## Versioning with Changesets
111
+
112
+ Spree's own packages are versioned with [Changesets](https://github.com/changesets/changesets); plugin authors don't have to use it, but it pairs well with peer-dep ranges. Workflow:
113
+
114
+ ```bash
115
+ pnpm changeset # describe what changed → writes a markdown file
116
+ git commit -am "feat: x"
117
+ pnpm changeset version # bumps package versions + writes CHANGELOG
118
+ pnpm install # resolve workspace versions
119
+ git commit -am "Release"
120
+ pnpm publish -r # publish updated packages
121
+ ```
122
+
123
+ Standalone plugin repos can run Changesets the same way; the CLI scaffold doesn't pre-configure it.
124
+
125
+ ## Publishing the npm package
126
+
127
+ ```bash
128
+ pnpm build
129
+ pnpm publish --access public
130
+ ```
131
+
132
+ Set `"private": false` and remove the `"publishConfig.access": "restricted"` if your scaffold has it.
133
+
134
+ ## Avoiding lock-step releases
135
+
136
+ Your plugin should compile against a **range** of dashboard versions. Run the build against the lowest supported version locally (`pnpm add -D @spree/dashboard-core@6.0.0`) and add a CI matrix job that bumps to the next minor and rebuilds. That's the cheapest way to catch "I depended on something that doesn't exist in 6.0.0".
137
+
138
+ ## Reference
139
+
140
+ - [npm `peerDependencies` docs](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies)
141
+ - [Changesets](https://github.com/changesets/changesets)
142
+ - [tsup docs](https://tsup.egoist.dev/)
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: Scaffolding
3
+ sidebarTitle: Scaffolding
4
+ description: Generate a new dashboard plugin monorepo with `@spree/cli` in one command. Includes TypeScript, biome, tsup, a working example entry-point, and translations.
5
+ ---
6
+
7
+ `@spree/cli` ships a `plugin new` command that scaffolds the whole plugin monorepo for you. Generated layout:
8
+
9
+ ```
10
+ your-plugin/
11
+ ├── package.json # workspace root
12
+ ├── pnpm-workspace.yaml
13
+ ├── LICENSE
14
+ ├── README.md
15
+ ├── .gitignore
16
+ └── packages/
17
+ └── dashboard/
18
+ ├── package.json # @your-scope/dashboard-your-plugin
19
+ ├── tsconfig.json
20
+ ├── biome.json
21
+ └── src/
22
+ ├── index.tsx # defineDashboardPlugin entry
23
+ ├── client.ts # adminClient request wrappers
24
+ ├── types.ts # plugin-specific TS types
25
+ ├── pages/
26
+ │ └── <name>-list.tsx
27
+ ├── routes/ # TanStack file routes, compiled into the host's typed route tree
28
+ │ └── <name>.index.tsx
29
+ ├── slots/
30
+ │ └── product-<name>-card.tsx
31
+ └── locales/
32
+ └── en.json
33
+ ```
34
+
35
+ You get a working "Brands"-style plugin out of the box: nav entry, a list page wired through React Query, a product-detail slot card, and English translations.
36
+
37
+ ## Run it
38
+
39
+ ```bash
40
+ npx @spree/cli plugin new my-plugin
41
+ ```
42
+
43
+ The CLI prompts for:
44
+
45
+ - **Plugin name** — slug (kebab-case), used for package names and i18n keys
46
+ - **Ruby gem name** — only matters if you later add a Rails engine
47
+ - **Module name** — PascalCase, used as the React component prefix (`MyPluginCard`)
48
+ - **npm scope** — e.g., `@acme`
49
+ - **Author + email** — for `package.json` and `LICENSE`; prefilled from `git config user.name` / `user.email`
50
+ - **License** — MIT, Apache-2.0, or BSD-3-Clause (MIT by default); the CLI fills in the standard text
51
+
52
+ Every prompt has a matching flag:
53
+
54
+ ```bash
55
+ npx @spree/cli plugin new my-plugin \
56
+ --ruby-name spree_my_plugin \
57
+ --module-name MyPlugin \
58
+ --npm-scope @acme \
59
+ --author "Ada Lovelace" \
60
+ --author-email "ada@example.com" \
61
+ --license MIT \
62
+ --no-install # skip pnpm install
63
+ ```
64
+
65
+ For CI and scripted use, `-y` / `--yes` accepts the default for anything the flags leave unanswered — author and email come from your git config, everything else derives from the plugin name:
66
+
67
+ ```bash
68
+ npx @spree/cli plugin new my-plugin -y
69
+ ```
70
+
71
+ `--force` overwrites an existing directory. `--no-dashboard` skips the dashboard package (Rails-engine-only plugins). `--no-engine` is reserved — engine scaffolding lands later.
72
+
73
+ ## After scaffolding
74
+
75
+ ```bash
76
+ cd my-plugin
77
+ pnpm install # runs by default unless --no-install
78
+ pnpm build
79
+ ```
80
+
81
+ You now have a buildable plugin. The next page covers what to put in `package.json` for publishing — but if you're using it internally and importing by path, you can stop here and skip [Publishing](publishing.md).
82
+
83
+ ## What the entry-point looks like
84
+
85
+ `packages/dashboard/src/index.tsx` is where everything starts. The generated version, trimmed:
86
+
87
+ ```tsx
88
+ import { defineDashboardPlugin, defineTable, i18n } from '@spree/dashboard-core'
89
+ import { PackageIcon } from 'lucide-react'
90
+
91
+ import { MyPluginCard } from './slots/product-my-plugin-card'
92
+
93
+ // 1. Translations — merge our keys into the shared bundle.
94
+ i18n.addResourceBundle('en', 'translation', en, true, true)
95
+
96
+ // 2. Declare the table our list page renders with <ResourceTable>.
97
+ defineTable('my-plugin', {
98
+ title: i18n.t('admin.my_plugin.table.title'),
99
+ defaultSort: { field: 'name', direction: 'asc' },
100
+ columns: [/* … */],
101
+ })
102
+
103
+ // 3. Registry extensions. Routes are NOT registered here — they ship as
104
+ // file routes in src/routes/ (declared via the package.json marker) and
105
+ // compile into the host's typed route tree. See the routes guide.
106
+ defineDashboardPlugin({
107
+ nav: [{
108
+ key: 'my-plugin',
109
+ label: i18n.t('admin.my_plugin.title'),
110
+ path: '/my-plugin',
111
+ icon: PackageIcon,
112
+ position: 250,
113
+ }],
114
+ slots: {
115
+ 'product.form_sidebar': [{
116
+ id: 'my-plugin-card',
117
+ component: MyPluginCard as never,
118
+ position: 50,
119
+ }],
120
+ },
121
+ })
122
+ ```
123
+
124
+ That's the whole entry-point. Edit it to register what your plugin needs and remove what it doesn't.
125
+
126
+ ## The example endpoints
127
+
128
+ The generated `client.ts` calls `adminClient.request()` against `/<name>` (relative to `/api/v3/admin`) — those endpoints don't exist by default. You'd add them on the Rails side (a controller, a model, a serializer) for the plugin to do anything end-to-end. The CLI doesn't scaffold the Rails engine today; for now, write that part by hand using the [backend integration docs](../customization/backend.md).
129
+
130
+ ## Reference
131
+
132
+ - [`@spree/cli` source](https://github.com/spree/spree/tree/main/packages/cli)
133
+ - [Template tree](https://github.com/spree/spree/tree/main/packages/cli/templates/plugin) — what gets copied (`.tt` files are template-substituted)