@spree/dashboard-core 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +418 -0
- package/package.json +84 -0
- package/src/client.ts +25 -0
- package/src/components/address-form-dialog.tsx +376 -0
- package/src/components/app-sidebar.tsx +70 -0
- package/src/components/bulk-action-bar.tsx +504 -0
- package/src/components/can.tsx +30 -0
- package/src/components/country-combobox.tsx +238 -0
- package/src/components/country-state-fields.tsx +86 -0
- package/src/components/currency-select.tsx +181 -0
- package/src/components/export-button.tsx +156 -0
- package/src/components/file-upload-field.tsx +265 -0
- package/src/components/image-upload-field.tsx +53 -0
- package/src/components/import-button.tsx +202 -0
- package/src/components/locale-select.tsx +268 -0
- package/src/components/market-combobox.tsx +89 -0
- package/src/components/nav-main.tsx +186 -0
- package/src/components/page-header.tsx +286 -0
- package/src/components/page-tabs.tsx +78 -0
- package/src/components/preferences-form.tsx +404 -0
- package/src/components/resource-combobox.tsx +126 -0
- package/src/components/resource-multi-autocomplete.tsx +176 -0
- package/src/components/resource-picker-sheet.tsx +235 -0
- package/src/components/resource-table.tsx +652 -0
- package/src/components/settings-sidebar.tsx +139 -0
- package/src/components/slot.tsx +50 -0
- package/src/components/store-date-picker.tsx +18 -0
- package/src/components/store-switcher.tsx +70 -0
- package/src/components/table-toolbar.tsx +818 -0
- package/src/components/tag-combobox.tsx +99 -0
- package/src/components/top-bar.tsx +210 -0
- package/src/hooks/use-auth.ts +10 -0
- package/src/hooks/use-command-palette.tsx +34 -0
- package/src/hooks/use-countries.ts +15 -0
- package/src/hooks/use-custom-fields.ts +142 -0
- package/src/hooks/use-direct-upload.ts +104 -0
- package/src/hooks/use-display-name.ts +42 -0
- package/src/hooks/use-export.ts +75 -0
- package/src/hooks/use-global-search.ts +79 -0
- package/src/hooks/use-host-form.ts +34 -0
- package/src/hooks/use-import.ts +62 -0
- package/src/hooks/use-resource-mutation.ts +99 -0
- package/src/hooks/use-switch-admin-locale.ts +55 -0
- package/src/index.ts +97 -0
- package/src/lib/custom-field-components.ts +56 -0
- package/src/lib/download.ts +49 -0
- package/src/lib/filters-to-ransack.ts +47 -0
- package/src/lib/form-errors.ts +103 -0
- package/src/lib/form-fields-registry.ts +102 -0
- package/src/lib/form-mappers.ts +30 -0
- package/src/lib/formatters.ts +19 -0
- package/src/lib/i18n.ts +210 -0
- package/src/lib/nav-registry.ts +224 -0
- package/src/lib/permissions.ts +55 -0
- package/src/lib/query-client.ts +11 -0
- package/src/lib/query-keys.ts +69 -0
- package/src/lib/route-registry.ts +158 -0
- package/src/lib/search-registry.ts +129 -0
- package/src/lib/settings-nav-registry.ts +158 -0
- package/src/lib/slot-registry.ts +116 -0
- package/src/lib/table-registry.ts +274 -0
- package/src/lib/timestamp-columns.tsx +58 -0
- package/src/locales/ar.json +629 -0
- package/src/locales/de.json +629 -0
- package/src/locales/en.json +629 -0
- package/src/locales/fr.json +629 -0
- package/src/locales/pl.json +629 -0
- package/src/locales/zh-CN.json +629 -0
- package/src/plugin.ts +222 -0
- package/src/providers/auth-provider.tsx +188 -0
- package/src/providers/permission-provider.tsx +118 -0
- package/src/providers/store-provider.tsx +111 -0
- package/src/virtual.d.ts +5 -0
- package/src/vite/discover.ts +170 -0
- package/src/vite/index.ts +270 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Walk a host project's `package.json` dependencies and return the names of
|
|
8
|
+
* packages that opt-in as dashboard plugins by declaring
|
|
9
|
+
* `"spree": { "dashboard": { "plugin": true } }` in their own `package.json`.
|
|
10
|
+
*
|
|
11
|
+
* Auto-discovery is what makes `pnpm add @acme/foo-plugin` a one-step
|
|
12
|
+
* install: the discovered list feeds both Tailwind source scanning and the
|
|
13
|
+
* `virtual:spree-dashboard-plugins` activation module the app entry imports
|
|
14
|
+
* (see index.ts). Plugin authors opt-in by adding the marker field; hosts
|
|
15
|
+
* never have to know it exists.
|
|
16
|
+
*
|
|
17
|
+
* Order is deterministic — the order packages appear in the host's combined
|
|
18
|
+
* `dependencies` + `devDependencies` maps. Tailwind class scanning is
|
|
19
|
+
* order-insensitive, so this only matters for the dev-mode banner's listing.
|
|
20
|
+
*
|
|
21
|
+
* The function does not throw on missing files or malformed JSON — failures
|
|
22
|
+
* are reported via the `onWarn` callback and the offending package is
|
|
23
|
+
* skipped. A missing host `package.json` returns an empty array.
|
|
24
|
+
*/
|
|
25
|
+
export interface DiscoverOptions {
|
|
26
|
+
/** Project root to scan. Usually Vite's `config.root`. */
|
|
27
|
+
root: string
|
|
28
|
+
/** Optional callback for per-package warnings (malformed JSON, missing dep). */
|
|
29
|
+
onWarn?: (message: string) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A discovered plugin plus the build-relevant fields from its marker. */
|
|
33
|
+
export interface DashboardPluginManifest {
|
|
34
|
+
/** Package name, as it appears in the host's dependencies. */
|
|
35
|
+
name: string
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path to the plugin's file-routes directory, when the marker
|
|
38
|
+
* declares one (`"spree": { "dashboard": { "routes": "./src/routes" } }`).
|
|
39
|
+
* Compiled into the host's route tree by `@spree/dashboard/vite`.
|
|
40
|
+
*/
|
|
41
|
+
routesDir?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Like {@link discoverDashboardPlugins}, but returns each plugin's manifest
|
|
46
|
+
* fields. `names` restricts resolution to an explicit whitelist (the
|
|
47
|
+
* `plugins:` option) instead of walking the host's dependencies.
|
|
48
|
+
*/
|
|
49
|
+
export function discoverDashboardPluginManifests(
|
|
50
|
+
{ root, onWarn }: DiscoverOptions,
|
|
51
|
+
names?: string[],
|
|
52
|
+
): DashboardPluginManifest[] {
|
|
53
|
+
const require = createRequire(path.join(root, 'package.json'))
|
|
54
|
+
const candidates = names ?? discoverDashboardPlugins({ root, onWarn })
|
|
55
|
+
|
|
56
|
+
const manifests: DashboardPluginManifest[] = []
|
|
57
|
+
for (const name of candidates) {
|
|
58
|
+
const manifestPath = resolveManifest(name, require)
|
|
59
|
+
if (!manifestPath) {
|
|
60
|
+
manifests.push({ name })
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
let pkg: PluginManifest
|
|
64
|
+
try {
|
|
65
|
+
pkg = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as PluginManifest
|
|
66
|
+
} catch (err) {
|
|
67
|
+
onWarn?.(`Could not parse ${manifestPath}: ${(err as Error).message}`)
|
|
68
|
+
manifests.push({ name })
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
const routes = pkg.spree?.dashboard?.routes
|
|
72
|
+
manifests.push({
|
|
73
|
+
name,
|
|
74
|
+
routesDir: routes ? path.resolve(path.dirname(manifestPath), routes) : undefined,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return manifests
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function discoverDashboardPlugins({ root, onWarn }: DiscoverOptions): string[] {
|
|
81
|
+
const hostPkgPath = path.join(root, 'package.json')
|
|
82
|
+
if (!fs.existsSync(hostPkgPath)) return []
|
|
83
|
+
|
|
84
|
+
let hostPkg: HostManifest
|
|
85
|
+
try {
|
|
86
|
+
hostPkg = JSON.parse(fs.readFileSync(hostPkgPath, 'utf8')) as HostManifest
|
|
87
|
+
} catch (err) {
|
|
88
|
+
onWarn?.(`Could not parse ${hostPkgPath}: ${(err as Error).message}`)
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const deps = [
|
|
93
|
+
...Object.keys(hostPkg.dependencies ?? {}),
|
|
94
|
+
...Object.keys(hostPkg.devDependencies ?? {}),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
// Resolve from the host's own root so workspace symlinks and `pnpm`'s
|
|
98
|
+
// `.pnpm/` hoisting both work. createRequire wants a file path, not a
|
|
99
|
+
// directory, so synthesize one.
|
|
100
|
+
const require = createRequire(path.join(root, 'package.json'))
|
|
101
|
+
|
|
102
|
+
const discovered: string[] = []
|
|
103
|
+
const seen = new Set<string>()
|
|
104
|
+
|
|
105
|
+
for (const name of deps) {
|
|
106
|
+
if (seen.has(name)) continue
|
|
107
|
+
seen.add(name)
|
|
108
|
+
|
|
109
|
+
const manifestPath = resolveManifest(name, require)
|
|
110
|
+
if (!manifestPath) continue
|
|
111
|
+
|
|
112
|
+
let depPkg: PluginManifest
|
|
113
|
+
try {
|
|
114
|
+
depPkg = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as PluginManifest
|
|
115
|
+
} catch (err) {
|
|
116
|
+
onWarn?.(`Could not parse ${manifestPath}: ${(err as Error).message}`)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (depPkg.spree?.dashboard?.plugin === true) {
|
|
121
|
+
discovered.push(name)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return discovered
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface HostManifest {
|
|
129
|
+
dependencies?: Record<string, string>
|
|
130
|
+
devDependencies?: Record<string, string>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface PluginManifest {
|
|
134
|
+
spree?: {
|
|
135
|
+
dashboard?: {
|
|
136
|
+
plugin?: boolean
|
|
137
|
+
routes?: string
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve `<pkg>/package.json` to its filesystem path. We can't always do
|
|
144
|
+
* `require.resolve('<pkg>/package.json')` because many packages don't expose
|
|
145
|
+
* it in their `exports` map. Fall back to walking up from the resolved entry
|
|
146
|
+
* point — same pattern as the source-dir resolver in `index.ts`.
|
|
147
|
+
*/
|
|
148
|
+
function resolveManifest(pkg: string, require: NodeJS.Require): string | null {
|
|
149
|
+
try {
|
|
150
|
+
return require.resolve(`${pkg}/package.json`)
|
|
151
|
+
} catch {
|
|
152
|
+
// exports map didn't include package.json; walk up from the main entry.
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let entry: string
|
|
156
|
+
try {
|
|
157
|
+
entry = require.resolve(pkg)
|
|
158
|
+
} catch {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let current = path.dirname(entry.startsWith('file:') ? fileURLToPath(entry) : entry)
|
|
163
|
+
const { root } = path.parse(current)
|
|
164
|
+
while (current !== root) {
|
|
165
|
+
const candidate = path.join(current, 'package.json')
|
|
166
|
+
if (fs.existsSync(candidate)) return candidate
|
|
167
|
+
current = path.dirname(current)
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
// Self-referencing package import (not a relative path): this entry is loaded
|
|
6
|
+
// raw by Node when a host's vite.config.ts imports `@spree/dashboard-core/vite`,
|
|
7
|
+
// and Node's TS type-stripping can't resolve extensionless relative specifiers.
|
|
8
|
+
// The exports-map path resolves to the real file name everywhere.
|
|
9
|
+
import { discoverDashboardPlugins } from '@spree/dashboard-core/vite/discover'
|
|
10
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
11
|
+
import type { Plugin, PluginOption } from 'vite'
|
|
12
|
+
|
|
13
|
+
export interface SpreeDashboardPluginOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Path to the host app's CSS entry file (the one that does
|
|
16
|
+
* `@import "@spree/dashboard-ui/styles.css"`). Resolved relative to the
|
|
17
|
+
* host's project root. The plugin injects `@source` directives into this
|
|
18
|
+
* file so Tailwind v4 scans every Spree dashboard package and every
|
|
19
|
+
* dashboard plugin for class usage.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to `./src/styles.css`, which fits the conventional Vite layout.
|
|
22
|
+
*/
|
|
23
|
+
cssEntry?: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Names of dashboard plugin packages installed in the host app. Each name
|
|
27
|
+
* must be a resolvable npm package specifier — the same string you'd pass
|
|
28
|
+
* to `import()`. The plugin resolves each one via Node module resolution
|
|
29
|
+
* and tells Tailwind v4 to scan its source files for class usage.
|
|
30
|
+
*
|
|
31
|
+
* When omitted (the typical case), the plugin auto-discovers installed
|
|
32
|
+
* dashboard plugins by walking the host's `package.json` dependencies and
|
|
33
|
+
* picking up anything that declares
|
|
34
|
+
* `"spree": { "dashboard": { "plugin": true } }`. Pass an explicit array
|
|
35
|
+
* to opt out of auto-discovery — useful for whitelisting in tightly
|
|
36
|
+
* controlled environments.
|
|
37
|
+
*
|
|
38
|
+
* Example: `['@my-store/orders-plugin', '@my-store/wishlists-plugin']`.
|
|
39
|
+
*/
|
|
40
|
+
plugins?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Vite plugin that wires up a Spree dashboard's plugin system and Tailwind v4.
|
|
45
|
+
*
|
|
46
|
+
* Combines three concerns:
|
|
47
|
+
*
|
|
48
|
+
* 1. Plugin activation: serves `virtual:spree-dashboard-plugins`, a
|
|
49
|
+
* synthesized module that side-effect-imports every dashboard plugin
|
|
50
|
+
* (auto-discovered or whitelisted). The app entry imports it once —
|
|
51
|
+
* after that, installing a plugin is just `pnpm add`; its
|
|
52
|
+
* `defineDashboardPlugin` call runs without any host-code edit.
|
|
53
|
+
*
|
|
54
|
+
* 2. Source-scanning: tells Tailwind to scan `@spree/dashboard-core`,
|
|
55
|
+
* `@spree/dashboard-ui`, and every plugin package, by injecting
|
|
56
|
+
* `@source` directives into the host's CSS entry. Tailwind v4
|
|
57
|
+
* doesn't scan `node_modules` by default and only accepts filesystem
|
|
58
|
+
* paths in `@source` directives (not bare package specifiers), so we
|
|
59
|
+
* resolve each package through Node module resolution to get an
|
|
60
|
+
* absolute path that works under any package layout (workspace
|
|
61
|
+
* symlinks, npm tarballs, pnpm's `.pnpm/` hoisting).
|
|
62
|
+
*
|
|
63
|
+
* 3. Tailwind itself: returns `@tailwindcss/vite` bundled alongside the
|
|
64
|
+
* source-injection plugin, in the right order. Hosts don't add
|
|
65
|
+
* `@tailwindcss/vite` separately.
|
|
66
|
+
*
|
|
67
|
+
* Usage from a host using the full `@spree/dashboard` app shell:
|
|
68
|
+
*
|
|
69
|
+
* // host vite.config.ts
|
|
70
|
+
* import { spreeDashboardPlugin } from '@spree/dashboard-core/vite'
|
|
71
|
+
*
|
|
72
|
+
* export default defineConfig({
|
|
73
|
+
* plugins: [
|
|
74
|
+
* spreeDashboardPlugin(),
|
|
75
|
+
* // … react(), TanStack Router, etc. — host owns these.
|
|
76
|
+
* ],
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* // host src/main.tsx (once, before the router mounts)
|
|
80
|
+
* import 'virtual:spree-dashboard-plugins'
|
|
81
|
+
*
|
|
82
|
+
* For a custom dashboard built on `@spree/dashboard-core` + `@spree/dashboard-ui`,
|
|
83
|
+
* point `cssEntry` at the host's own CSS file:
|
|
84
|
+
*
|
|
85
|
+
* spreeDashboardPlugin({
|
|
86
|
+
* cssEntry: './src/admin.css',
|
|
87
|
+
* plugins: ['@my-store/vendor-portal-plugin'], // explicit whitelist opt-out
|
|
88
|
+
* })
|
|
89
|
+
*/
|
|
90
|
+
export function spreeDashboardPlugin(options: SpreeDashboardPluginOptions = {}): PluginOption[] {
|
|
91
|
+
return [dashboardTailwindSourcePlugin(options), tailwindcss()]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Module specifier hosts import to activate discovered dashboard plugins. */
|
|
95
|
+
export const VIRTUAL_PLUGIN_MODULE_ID = 'virtual:spree-dashboard-plugins'
|
|
96
|
+
// Rollup convention: \0 marks the resolved id as virtual so other plugins
|
|
97
|
+
// (and Vite's own resolver) leave it alone.
|
|
98
|
+
const RESOLVED_VIRTUAL_PLUGIN_MODULE_ID = `\0${VIRTUAL_PLUGIN_MODULE_ID}`
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Source of `virtual:spree-dashboard-plugins`: one side-effect import per
|
|
102
|
+
* activatable plugin, in discovery order. Exported for tests.
|
|
103
|
+
*/
|
|
104
|
+
export function generateVirtualPluginModule(packages: readonly string[]): string {
|
|
105
|
+
const header = '// generated by @spree/dashboard-core/vite — one import per dashboard plugin'
|
|
106
|
+
if (packages.length === 0) return `${header}\nexport {}\n`
|
|
107
|
+
const imports = packages.map((pkg) => `import ${JSON.stringify(pkg)}`).join('\n')
|
|
108
|
+
return `${header}\n${imports}\nexport {}\n`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function dashboardTailwindSourcePlugin(options: SpreeDashboardPluginOptions): Plugin {
|
|
112
|
+
const explicitPlugins = options.plugins
|
|
113
|
+
const cssEntry = options.cssEntry ?? './src/styles.css'
|
|
114
|
+
|
|
115
|
+
// Resolution root for the framework's own siblings. Wherever
|
|
116
|
+
// `@spree/dashboard-core` ends up on disk (workspace symlink, hoisted
|
|
117
|
+
// npm install, pnpm `.pnpm/` directory), `@spree/dashboard-ui` is
|
|
118
|
+
// reachable from here via Node module resolution.
|
|
119
|
+
const fromHere = createRequire(import.meta.url)
|
|
120
|
+
|
|
121
|
+
let resolvedSources: string[] = []
|
|
122
|
+
let resolvedCssEntry = ''
|
|
123
|
+
let pluginErrors: PluginError[] = []
|
|
124
|
+
let activatablePlugins: string[] = []
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
name: 'spree:dashboard-tailwind-source',
|
|
128
|
+
enforce: 'pre',
|
|
129
|
+
|
|
130
|
+
configResolved(config) {
|
|
131
|
+
resolvedCssEntry = path.resolve(config.root, cssEntry).split(path.sep).join('/')
|
|
132
|
+
|
|
133
|
+
// `plugins: []` is an explicit empty whitelist; only `plugins` omitted
|
|
134
|
+
// triggers discovery. Discovery walks the host's package.json deps and
|
|
135
|
+
// returns names that opt-in via the `spree.dashboard.plugin` marker.
|
|
136
|
+
const pluginPackages =
|
|
137
|
+
explicitPlugins ??
|
|
138
|
+
discoverDashboardPlugins({
|
|
139
|
+
root: config.root,
|
|
140
|
+
onWarn: (msg) => console.warn(`[@spree/dashboard-core/vite] ${msg}`),
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Plugins are the host's dependencies, so resolve them from the host
|
|
144
|
+
// root — under pnpm's strict layout they are not necessarily reachable
|
|
145
|
+
// from dashboard-core's own location.
|
|
146
|
+
const fromHost = createRequire(path.join(config.root, 'package.json'))
|
|
147
|
+
|
|
148
|
+
pluginErrors = []
|
|
149
|
+
resolvedSources = []
|
|
150
|
+
activatablePlugins = []
|
|
151
|
+
for (const pkg of ['@spree/dashboard-core', '@spree/dashboard-ui']) {
|
|
152
|
+
const dir = resolvePackageSourceDir(pkg, fromHere)
|
|
153
|
+
if (dir) {
|
|
154
|
+
resolvedSources.push(dir)
|
|
155
|
+
} else {
|
|
156
|
+
pluginErrors.push({ pkg, kind: 'unresolved' })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// The app shell is optional: present when the host builds on the full
|
|
160
|
+
// `@spree/dashboard` package, absent for custom dashboards composed
|
|
161
|
+
// directly from core + ui — hence no error when unresolved.
|
|
162
|
+
const shellDir = resolvePackageSourceDir('@spree/dashboard', fromHost)
|
|
163
|
+
if (shellDir) resolvedSources.push(shellDir)
|
|
164
|
+
for (const pkg of pluginPackages) {
|
|
165
|
+
const dir = resolvePackageSourceDir(pkg, fromHost)
|
|
166
|
+
if (dir) {
|
|
167
|
+
resolvedSources.push(dir)
|
|
168
|
+
activatablePlugins.push(pkg)
|
|
169
|
+
} else {
|
|
170
|
+
// Unresolved plugins are reported via the dev overlay instead of
|
|
171
|
+
// being imported — a bare failing import would crash the dev
|
|
172
|
+
// server with a much less actionable error.
|
|
173
|
+
pluginErrors.push({ pkg, kind: 'unresolved' })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
resolveId(id) {
|
|
179
|
+
if (id === VIRTUAL_PLUGIN_MODULE_ID) return RESOLVED_VIRTUAL_PLUGIN_MODULE_ID
|
|
180
|
+
return null
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
load(id) {
|
|
184
|
+
if (id !== RESOLVED_VIRTUAL_PLUGIN_MODULE_ID) return null
|
|
185
|
+
return generateVirtualPluginModule(activatablePlugins)
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
transform(code, id) {
|
|
189
|
+
const normalized = id.split('?')[0].replaceAll('\\', '/')
|
|
190
|
+
if (normalized !== resolvedCssEntry) return null
|
|
191
|
+
|
|
192
|
+
const directives = resolvedSources.map((dir) => `@source '${dir}';`).join('\n')
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
code: `${code}\n\n/* injected by @spree/dashboard-core/vite */\n${directives}\n`,
|
|
196
|
+
map: null,
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
configureServer(server) {
|
|
201
|
+
// Surface unresolved plugins as a Vite error overlay in dev so the
|
|
202
|
+
// failure mode is loud. Without this, a missing plugin silently emits
|
|
203
|
+
// unstyled HTML when the host imports it. Sent once per client
|
|
204
|
+
// connection rather than per HTTP request.
|
|
205
|
+
if (pluginErrors.length === 0) return
|
|
206
|
+
const message = formatPluginErrorOverlay(pluginErrors)
|
|
207
|
+
server.ws.on('connection', () => {
|
|
208
|
+
server.ws.send({
|
|
209
|
+
type: 'error',
|
|
210
|
+
err: {
|
|
211
|
+
message,
|
|
212
|
+
stack: '',
|
|
213
|
+
plugin: 'spree:dashboard-tailwind-source',
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface PluginError {
|
|
222
|
+
pkg: string
|
|
223
|
+
kind: 'unresolved'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatPluginErrorOverlay(errors: PluginError[]): string {
|
|
227
|
+
const lines = [
|
|
228
|
+
'Spree dashboard plugin(s) could not be resolved:',
|
|
229
|
+
'',
|
|
230
|
+
...errors.map(({ pkg }) => ` - ${pkg}`),
|
|
231
|
+
'',
|
|
232
|
+
'For each one, check:',
|
|
233
|
+
' 1. The package is installed (`pnpm add <package>`)',
|
|
234
|
+
' 2. Its package.json declares the dashboard marker:',
|
|
235
|
+
' { "spree": { "dashboard": { "plugin": true } } }',
|
|
236
|
+
'',
|
|
237
|
+
'If you registered it explicitly via spreeDashboardPlugin({ plugins: [...] }),',
|
|
238
|
+
'verify the name matches the installed package exactly.',
|
|
239
|
+
]
|
|
240
|
+
return lines.join('\n')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve `<package>/src` on disk. Resolves the package's main entry point
|
|
245
|
+
* (defined by its `exports` map or `main` field) and walks upwards looking
|
|
246
|
+
* for `package.json` to find the package root. Avoids requiring
|
|
247
|
+
* `<pkg>/package.json` to be in the package's `exports` map — many packages,
|
|
248
|
+
* including ours, don't list it there.
|
|
249
|
+
*/
|
|
250
|
+
function resolvePackageSourceDir(pkg: string, require: NodeJS.Require): string | null {
|
|
251
|
+
try {
|
|
252
|
+
const entryPath = require.resolve(pkg)
|
|
253
|
+
const entryFile = entryPath.startsWith('file:') ? fileURLToPath(entryPath) : entryPath
|
|
254
|
+
const packageRoot = findPackageRoot(entryFile)
|
|
255
|
+
if (!packageRoot) return null
|
|
256
|
+
return path.join(packageRoot, 'src')
|
|
257
|
+
} catch {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findPackageRoot(fromFile: string): string | null {
|
|
263
|
+
let current = path.dirname(fromFile)
|
|
264
|
+
const { root } = path.parse(current)
|
|
265
|
+
while (current !== root) {
|
|
266
|
+
if (fs.existsSync(path.join(current, 'package.json'))) return current
|
|
267
|
+
current = path.dirname(current)
|
|
268
|
+
}
|
|
269
|
+
return null
|
|
270
|
+
}
|