@mantajs/dashboard 0.1.14 → 0.1.16
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/README.md +86 -0
- package/dist/vite-plugin/index.js +137 -50
- package/dist/vite-plugin/index.mjs +137 -50
- package/package.json +1 -1
- package/src/vite-plugin/index.ts +191 -70
package/README.md
CHANGED
|
@@ -205,6 +205,92 @@ import type { MenuConfig, MenuItem, MenuNestedItem } from "@mantajs/dashboard/vi
|
|
|
205
205
|
|
|
206
206
|
When no `menu.config.ts` is found, the dashboard falls back to its built-in sidebar menu.
|
|
207
207
|
|
|
208
|
+
### Menu, Nested Routes, and Modules: How They Interact
|
|
209
|
+
|
|
210
|
+
Understanding how the sidebar menu is built is critical to avoid duplicate or missing entries. There are **three sources** that can add items to the sidebar:
|
|
211
|
+
|
|
212
|
+
1. **Your `menu.config.tsx`** — the custom menu you define
|
|
213
|
+
2. **Route configs with `nested`** — pages that declare `nested: "/parent"` in `defineRouteConfig()`
|
|
214
|
+
3. **Plugin modules** — modules like `@medusajs/draft-order` that register their own routes and menu entries
|
|
215
|
+
|
|
216
|
+
#### How `nested` works
|
|
217
|
+
|
|
218
|
+
When a route page exports a config with `nested`, Medusa **automatically injects** it as a sub-item under the specified parent in the sidebar:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// src/admin/routes/draft-orders/page.tsx
|
|
222
|
+
export const config = defineRouteConfig({
|
|
223
|
+
label: "Drafts",
|
|
224
|
+
nested: "/orders", // ← auto-injected under Orders in the sidebar
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
This happens **regardless** of your `menu.config.tsx`. Even if you define a custom menu, any route with `nested` will still be injected as a child of its parent entry.
|
|
229
|
+
|
|
230
|
+
**To prevent a route from appearing in the sidebar**, remove the `nested` property:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
export const config = defineRouteConfig({
|
|
234
|
+
label: "Drafts Test",
|
|
235
|
+
// no `nested` → not auto-injected in the menu
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The page remains accessible via its URL (`/app/draft-orders`) but won't appear in the sidebar unless you explicitly add it to your menu config.
|
|
240
|
+
|
|
241
|
+
#### Controlling sub-items via `menu.config.tsx`
|
|
242
|
+
|
|
243
|
+
If you want full control over which sub-items appear under a menu entry, define them explicitly in `items`:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
{
|
|
247
|
+
icon: <ShoppingCart />,
|
|
248
|
+
label: "orders.domain",
|
|
249
|
+
useTranslation: true,
|
|
250
|
+
to: "/orders",
|
|
251
|
+
items: [
|
|
252
|
+
{ label: "Draft Orders", to: "/draft-orders" },
|
|
253
|
+
],
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Important:** Nested routes (`nested: "/orders"`) are still injected even if you define `items` manually. To avoid duplicates, either:
|
|
258
|
+
- Remove `nested` from the route config, **or**
|
|
259
|
+
- Don't list the route in `items` (let `nested` handle it)
|
|
260
|
+
|
|
261
|
+
Never do both — you'll get a duplicate entry.
|
|
262
|
+
|
|
263
|
+
#### Plugin modules and the Extensions section
|
|
264
|
+
|
|
265
|
+
Medusa plugin modules (e.g., `@medusajs/draft-order`) register their own sidebar entries. By default, these appear in the **Extensions** section at the bottom of the sidebar.
|
|
266
|
+
|
|
267
|
+
When you include a module's route in your `menu.config.tsx`, the module's entry is **absorbed** into your custom menu and no longer appears separately in Extensions:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Including /draft-orders in the custom menu prevents it from
|
|
271
|
+
// appearing again under Extensions
|
|
272
|
+
{
|
|
273
|
+
icon: <ShoppingCart />,
|
|
274
|
+
label: "Orders",
|
|
275
|
+
to: "/orders",
|
|
276
|
+
items: [
|
|
277
|
+
{ label: "Draft Orders", to: "/draft-orders" }, // ← module route
|
|
278
|
+
],
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
If you **don't** include a module's route in your menu config, it will appear in the Extensions section as usual.
|
|
283
|
+
|
|
284
|
+
#### Summary
|
|
285
|
+
|
|
286
|
+
| Scenario | Result |
|
|
287
|
+
|----------|--------|
|
|
288
|
+
| Route has `nested: "/orders"` | Auto-injected under Orders in sidebar |
|
|
289
|
+
| Route has no `nested` | Not in sidebar (unless in `menu.config.tsx`) |
|
|
290
|
+
| Module route listed in `menu.config.tsx` | Appears in your menu, not in Extensions |
|
|
291
|
+
| Module route **not** in `menu.config.tsx` | Appears in Extensions section |
|
|
292
|
+
| Route has `nested` **and** listed in `items` | Duplicate entry (avoid this!) |
|
|
293
|
+
|
|
208
294
|
## Exports
|
|
209
295
|
|
|
210
296
|
| Import | Description |
|
|
@@ -86,9 +86,30 @@ function getComponentName(filePath) {
|
|
|
86
86
|
}
|
|
87
87
|
return baseName;
|
|
88
88
|
}
|
|
89
|
+
function findDashboardSrc() {
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const candidates = [
|
|
92
|
+
import_path.default.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
93
|
+
import_path.default.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
94
|
+
import_path.default.join(cwd, ".yalc", "@mantajs", "dashboard", "src")
|
|
95
|
+
];
|
|
96
|
+
for (const dir of candidates) {
|
|
97
|
+
if (import_fs.default.existsSync(dir)) return dir;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
89
101
|
function customDashboardPlugin() {
|
|
90
102
|
const componentsDir = import_path.default.resolve(process.cwd(), "src/admin/components");
|
|
91
103
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
104
|
+
const appliedOverrides = /* @__PURE__ */ new Set();
|
|
105
|
+
const knownDashboardComponents = /* @__PURE__ */ new Set();
|
|
106
|
+
const dashboardSrc = findDashboardSrc();
|
|
107
|
+
if (dashboardSrc) {
|
|
108
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
109
|
+
const cName = getComponentName(f);
|
|
110
|
+
if (cName) knownDashboardComponents.add(cName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
92
113
|
if (import_fs.default.existsSync(componentsDir)) {
|
|
93
114
|
const collectedFiles = collectComponentFiles(componentsDir).sort();
|
|
94
115
|
for (const fullPath of collectedFiles) {
|
|
@@ -105,8 +126,15 @@ function customDashboardPlugin() {
|
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
const hasOverrides = overridesByName.size > 0;
|
|
108
|
-
if (
|
|
109
|
-
|
|
129
|
+
if (process.env.NODE_ENV === "development") {
|
|
130
|
+
if (hasOverrides) {
|
|
131
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
132
|
+
}
|
|
133
|
+
if (knownDashboardComponents.size > 0) {
|
|
134
|
+
console.log(
|
|
135
|
+
`[custom-dashboard] Scanned ${knownDashboardComponents.size} dashboard components for override matching`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
110
138
|
}
|
|
111
139
|
return {
|
|
112
140
|
name: "custom-dashboard",
|
|
@@ -115,67 +143,126 @@ function customDashboardPlugin() {
|
|
|
115
143
|
config.optimizeDeps = config.optimizeDeps || {};
|
|
116
144
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || [];
|
|
117
145
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
146
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
|
|
147
|
+
config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
|
|
148
|
+
const overrides = overridesByName;
|
|
149
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
150
|
+
name: "dashboard-component-overrides",
|
|
151
|
+
setup(build) {
|
|
152
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
153
|
+
if (overrides.size === 0) return void 0;
|
|
154
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
155
|
+
if (!normalized.includes("/dashboard/dist/")) return void 0;
|
|
156
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
|
|
157
|
+
let contents;
|
|
158
|
+
try {
|
|
159
|
+
contents = import_fs.default.readFileSync(srcEntry, "utf-8");
|
|
160
|
+
} catch {
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
if (process.env.NODE_ENV === "development") {
|
|
164
|
+
console.log(
|
|
165
|
+
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
contents,
|
|
170
|
+
loader: "tsx",
|
|
171
|
+
resolveDir: import_path.default.dirname(srcEntry)
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
175
|
+
if (overrides.size === 0) return void 0;
|
|
176
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
177
|
+
if (!normalized.includes("/dashboard/src/")) return void 0;
|
|
178
|
+
const fileName = import_path.default.basename(args.path);
|
|
179
|
+
if (fileName.startsWith("index.")) return void 0;
|
|
180
|
+
const componentName = getComponentName(args.path);
|
|
181
|
+
if (componentName && overrides.has(componentName)) {
|
|
182
|
+
const overridePath = overrides.get(componentName);
|
|
183
|
+
const ext = import_path.default.extname(overridePath).slice(1);
|
|
184
|
+
const loader = VALID_LOADERS[ext] || "tsx";
|
|
129
185
|
let contents;
|
|
130
186
|
try {
|
|
131
|
-
contents = import_fs.default.readFileSync(
|
|
187
|
+
contents = import_fs.default.readFileSync(overridePath, "utf-8");
|
|
132
188
|
} catch {
|
|
133
189
|
return void 0;
|
|
134
190
|
}
|
|
191
|
+
appliedOverrides.add(componentName);
|
|
135
192
|
if (process.env.NODE_ENV === "development") {
|
|
136
193
|
console.log(
|
|
137
|
-
`[custom-dashboard]
|
|
194
|
+
`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
|
|
138
195
|
);
|
|
139
196
|
}
|
|
140
197
|
return {
|
|
141
198
|
contents,
|
|
142
|
-
loader
|
|
143
|
-
resolveDir: import_path.default.dirname(
|
|
199
|
+
loader,
|
|
200
|
+
resolveDir: import_path.default.dirname(overridePath)
|
|
144
201
|
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
config.optimizeDeps.force = true;
|
|
208
|
+
},
|
|
209
|
+
configureServer(server) {
|
|
210
|
+
if (!import_fs.default.existsSync(componentsDir)) return;
|
|
211
|
+
let debounceTimer = null;
|
|
212
|
+
const extractName = (file) => {
|
|
213
|
+
const normalized = file.replace(/\\/g, "/");
|
|
214
|
+
if (!normalized.startsWith(componentsDir.replace(/\\/g, "/"))) return null;
|
|
215
|
+
const ext = import_path.default.extname(file);
|
|
216
|
+
if (!COMPONENT_EXT_SET.has(ext)) return null;
|
|
217
|
+
const fileName = import_path.default.basename(file);
|
|
218
|
+
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
219
|
+
if (!name || name === "index") return null;
|
|
220
|
+
return name;
|
|
221
|
+
};
|
|
222
|
+
const triggerRestart = (name, reason) => {
|
|
223
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
224
|
+
debounceTimer = setTimeout(() => {
|
|
225
|
+
const newOverrides = /* @__PURE__ */ new Map();
|
|
226
|
+
const collectedFiles = collectComponentFiles(componentsDir).sort();
|
|
227
|
+
for (const fullPath of collectedFiles) {
|
|
228
|
+
const fn = import_path.default.basename(fullPath);
|
|
229
|
+
const n = fn.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
230
|
+
if (n && n !== "index") {
|
|
231
|
+
newOverrides.set(n, fullPath);
|
|
232
|
+
}
|
|
175
233
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
overridesByName.clear();
|
|
235
|
+
for (const [k, v] of newOverrides) overridesByName.set(k, v);
|
|
236
|
+
console.log(
|
|
237
|
+
`[custom-dashboard] Override "${name}" ${reason} \u2192 restarting Vite...`
|
|
238
|
+
);
|
|
239
|
+
console.log(
|
|
240
|
+
`[custom-dashboard] overrides:`,
|
|
241
|
+
[...overridesByName.keys()]
|
|
242
|
+
);
|
|
243
|
+
server.restart();
|
|
244
|
+
}, 300);
|
|
245
|
+
};
|
|
246
|
+
server.watcher.add(componentsDir);
|
|
247
|
+
server.watcher.on("add", (file) => {
|
|
248
|
+
const name = extractName(file);
|
|
249
|
+
if (name && knownDashboardComponents.has(name)) {
|
|
250
|
+
triggerRestart(name, "created");
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
server.watcher.on("change", (file) => {
|
|
254
|
+
const name = extractName(file);
|
|
255
|
+
if (name && appliedOverrides.has(name)) {
|
|
256
|
+
triggerRestart(name, "modified");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
server.watcher.on("unlink", (file) => {
|
|
260
|
+
const name = extractName(file);
|
|
261
|
+
if (name && appliedOverrides.has(name)) {
|
|
262
|
+
appliedOverrides.delete(name);
|
|
263
|
+
triggerRestart(name, "deleted");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
179
266
|
},
|
|
180
267
|
resolveId(source) {
|
|
181
268
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
|
@@ -51,9 +51,30 @@ function getComponentName(filePath) {
|
|
|
51
51
|
}
|
|
52
52
|
return baseName;
|
|
53
53
|
}
|
|
54
|
+
function findDashboardSrc() {
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
const candidates = [
|
|
57
|
+
path.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
58
|
+
path.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
59
|
+
path.join(cwd, ".yalc", "@mantajs", "dashboard", "src")
|
|
60
|
+
];
|
|
61
|
+
for (const dir of candidates) {
|
|
62
|
+
if (fs.existsSync(dir)) return dir;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
54
66
|
function customDashboardPlugin() {
|
|
55
67
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components");
|
|
56
68
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
69
|
+
const appliedOverrides = /* @__PURE__ */ new Set();
|
|
70
|
+
const knownDashboardComponents = /* @__PURE__ */ new Set();
|
|
71
|
+
const dashboardSrc = findDashboardSrc();
|
|
72
|
+
if (dashboardSrc) {
|
|
73
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
74
|
+
const cName = getComponentName(f);
|
|
75
|
+
if (cName) knownDashboardComponents.add(cName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
57
78
|
if (fs.existsSync(componentsDir)) {
|
|
58
79
|
const collectedFiles = collectComponentFiles(componentsDir).sort();
|
|
59
80
|
for (const fullPath of collectedFiles) {
|
|
@@ -70,8 +91,15 @@ function customDashboardPlugin() {
|
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
const hasOverrides = overridesByName.size > 0;
|
|
73
|
-
if (
|
|
74
|
-
|
|
94
|
+
if (process.env.NODE_ENV === "development") {
|
|
95
|
+
if (hasOverrides) {
|
|
96
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
97
|
+
}
|
|
98
|
+
if (knownDashboardComponents.size > 0) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[custom-dashboard] Scanned ${knownDashboardComponents.size} dashboard components for override matching`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
75
103
|
}
|
|
76
104
|
return {
|
|
77
105
|
name: "custom-dashboard",
|
|
@@ -80,67 +108,126 @@ function customDashboardPlugin() {
|
|
|
80
108
|
config.optimizeDeps = config.optimizeDeps || {};
|
|
81
109
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || [];
|
|
82
110
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
|
|
112
|
+
config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
|
|
113
|
+
const overrides = overridesByName;
|
|
114
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
115
|
+
name: "dashboard-component-overrides",
|
|
116
|
+
setup(build) {
|
|
117
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
118
|
+
if (overrides.size === 0) return void 0;
|
|
119
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
120
|
+
if (!normalized.includes("/dashboard/dist/")) return void 0;
|
|
121
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
|
|
122
|
+
let contents;
|
|
123
|
+
try {
|
|
124
|
+
contents = fs.readFileSync(srcEntry, "utf-8");
|
|
125
|
+
} catch {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
if (process.env.NODE_ENV === "development") {
|
|
129
|
+
console.log(
|
|
130
|
+
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
contents,
|
|
135
|
+
loader: "tsx",
|
|
136
|
+
resolveDir: path.dirname(srcEntry)
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
140
|
+
if (overrides.size === 0) return void 0;
|
|
141
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
142
|
+
if (!normalized.includes("/dashboard/src/")) return void 0;
|
|
143
|
+
const fileName = path.basename(args.path);
|
|
144
|
+
if (fileName.startsWith("index.")) return void 0;
|
|
145
|
+
const componentName = getComponentName(args.path);
|
|
146
|
+
if (componentName && overrides.has(componentName)) {
|
|
147
|
+
const overridePath = overrides.get(componentName);
|
|
148
|
+
const ext = path.extname(overridePath).slice(1);
|
|
149
|
+
const loader = VALID_LOADERS[ext] || "tsx";
|
|
94
150
|
let contents;
|
|
95
151
|
try {
|
|
96
|
-
contents = fs.readFileSync(
|
|
152
|
+
contents = fs.readFileSync(overridePath, "utf-8");
|
|
97
153
|
} catch {
|
|
98
154
|
return void 0;
|
|
99
155
|
}
|
|
156
|
+
appliedOverrides.add(componentName);
|
|
100
157
|
if (process.env.NODE_ENV === "development") {
|
|
101
158
|
console.log(
|
|
102
|
-
`[custom-dashboard]
|
|
159
|
+
`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
|
|
103
160
|
);
|
|
104
161
|
}
|
|
105
162
|
return {
|
|
106
163
|
contents,
|
|
107
|
-
loader
|
|
108
|
-
resolveDir: path.dirname(
|
|
164
|
+
loader,
|
|
165
|
+
resolveDir: path.dirname(overridePath)
|
|
109
166
|
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
}
|
|
168
|
+
return void 0;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
config.optimizeDeps.force = true;
|
|
173
|
+
},
|
|
174
|
+
configureServer(server) {
|
|
175
|
+
if (!fs.existsSync(componentsDir)) return;
|
|
176
|
+
let debounceTimer = null;
|
|
177
|
+
const extractName = (file) => {
|
|
178
|
+
const normalized = file.replace(/\\/g, "/");
|
|
179
|
+
if (!normalized.startsWith(componentsDir.replace(/\\/g, "/"))) return null;
|
|
180
|
+
const ext = path.extname(file);
|
|
181
|
+
if (!COMPONENT_EXT_SET.has(ext)) return null;
|
|
182
|
+
const fileName = path.basename(file);
|
|
183
|
+
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
184
|
+
if (!name || name === "index") return null;
|
|
185
|
+
return name;
|
|
186
|
+
};
|
|
187
|
+
const triggerRestart = (name, reason) => {
|
|
188
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
189
|
+
debounceTimer = setTimeout(() => {
|
|
190
|
+
const newOverrides = /* @__PURE__ */ new Map();
|
|
191
|
+
const collectedFiles = collectComponentFiles(componentsDir).sort();
|
|
192
|
+
for (const fullPath of collectedFiles) {
|
|
193
|
+
const fn = path.basename(fullPath);
|
|
194
|
+
const n = fn.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
195
|
+
if (n && n !== "index") {
|
|
196
|
+
newOverrides.set(n, fullPath);
|
|
197
|
+
}
|
|
140
198
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
overridesByName.clear();
|
|
200
|
+
for (const [k, v] of newOverrides) overridesByName.set(k, v);
|
|
201
|
+
console.log(
|
|
202
|
+
`[custom-dashboard] Override "${name}" ${reason} \u2192 restarting Vite...`
|
|
203
|
+
);
|
|
204
|
+
console.log(
|
|
205
|
+
`[custom-dashboard] overrides:`,
|
|
206
|
+
[...overridesByName.keys()]
|
|
207
|
+
);
|
|
208
|
+
server.restart();
|
|
209
|
+
}, 300);
|
|
210
|
+
};
|
|
211
|
+
server.watcher.add(componentsDir);
|
|
212
|
+
server.watcher.on("add", (file) => {
|
|
213
|
+
const name = extractName(file);
|
|
214
|
+
if (name && knownDashboardComponents.has(name)) {
|
|
215
|
+
triggerRestart(name, "created");
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
server.watcher.on("change", (file) => {
|
|
219
|
+
const name = extractName(file);
|
|
220
|
+
if (name && appliedOverrides.has(name)) {
|
|
221
|
+
triggerRestart(name, "modified");
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
server.watcher.on("unlink", (file) => {
|
|
225
|
+
const name = extractName(file);
|
|
226
|
+
if (name && appliedOverrides.has(name)) {
|
|
227
|
+
appliedOverrides.delete(name);
|
|
228
|
+
triggerRestart(name, "deleted");
|
|
229
|
+
}
|
|
230
|
+
});
|
|
144
231
|
},
|
|
145
232
|
resolveId(source) {
|
|
146
233
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
package/package.json
CHANGED
package/src/vite-plugin/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plugin } from "vite"
|
|
1
|
+
import { Plugin, ViteDevServer } from "vite"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import fs from "fs"
|
|
4
4
|
|
|
@@ -68,6 +68,23 @@ function getComponentName(filePath: string): string | null {
|
|
|
68
68
|
return baseName
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Find the dashboard source directory by checking known install paths.
|
|
73
|
+
* Works with yarn resolutions, direct installs, and yalc links.
|
|
74
|
+
*/
|
|
75
|
+
function findDashboardSrc(): string | null {
|
|
76
|
+
const cwd = process.cwd()
|
|
77
|
+
const candidates = [
|
|
78
|
+
path.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
79
|
+
path.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
80
|
+
path.join(cwd, ".yalc", "@mantajs", "dashboard", "src"),
|
|
81
|
+
]
|
|
82
|
+
for (const dir of candidates) {
|
|
83
|
+
if (fs.existsSync(dir)) return dir
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
/**
|
|
72
89
|
* Unified Vite plugin for @mantajs/dashboard.
|
|
73
90
|
*
|
|
@@ -80,6 +97,23 @@ export function customDashboardPlugin(): Plugin {
|
|
|
80
97
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components")
|
|
81
98
|
const overridesByName = new Map<string, string>()
|
|
82
99
|
|
|
100
|
+
// Track which overrides were actually matched during esbuild pre-bundling.
|
|
101
|
+
// Only these are real overrides — the rest are regular project components
|
|
102
|
+
// that happen to live in the same directory.
|
|
103
|
+
const appliedOverrides = new Set<string>()
|
|
104
|
+
|
|
105
|
+
// Scan dashboard source at startup to know all possible override targets.
|
|
106
|
+
// This lets the file watcher decide if a newly created file could be an
|
|
107
|
+
// override, without maintaining any hardcoded list.
|
|
108
|
+
const knownDashboardComponents = new Set<string>()
|
|
109
|
+
const dashboardSrc = findDashboardSrc()
|
|
110
|
+
if (dashboardSrc) {
|
|
111
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
112
|
+
const cName = getComponentName(f)
|
|
113
|
+
if (cName) knownDashboardComponents.add(cName)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
if (fs.existsSync(componentsDir)) {
|
|
84
118
|
const collectedFiles = collectComponentFiles(componentsDir).sort()
|
|
85
119
|
for (const fullPath of collectedFiles) {
|
|
@@ -98,8 +132,15 @@ export function customDashboardPlugin(): Plugin {
|
|
|
98
132
|
|
|
99
133
|
const hasOverrides = overridesByName.size > 0
|
|
100
134
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
135
|
+
if (process.env.NODE_ENV === "development") {
|
|
136
|
+
if (hasOverrides) {
|
|
137
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()])
|
|
138
|
+
}
|
|
139
|
+
if (knownDashboardComponents.size > 0) {
|
|
140
|
+
console.log(
|
|
141
|
+
`[custom-dashboard] Scanned ${knownDashboardComponents.size} dashboard components for override matching`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
103
144
|
}
|
|
104
145
|
|
|
105
146
|
return {
|
|
@@ -112,89 +153,169 @@ export function customDashboardPlugin(): Plugin {
|
|
|
112
153
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || []
|
|
113
154
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID)
|
|
114
155
|
|
|
115
|
-
if
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
// Always set up the esbuild override plugin — even if there are no
|
|
157
|
+
// overrides yet, the configureServer watcher may add some at runtime
|
|
158
|
+
// and trigger a restart.
|
|
159
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {}
|
|
160
|
+
config.optimizeDeps.esbuildOptions.plugins =
|
|
161
|
+
config.optimizeDeps.esbuildOptions.plugins || []
|
|
162
|
+
|
|
163
|
+
const overrides = overridesByName
|
|
164
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
165
|
+
name: "dashboard-component-overrides",
|
|
166
|
+
setup(build) {
|
|
167
|
+
// 1. Redirect the dist entry to source so esbuild processes
|
|
168
|
+
// individual TSX files instead of one big pre-built bundle.
|
|
169
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
170
|
+
// Only activate when there are overrides
|
|
171
|
+
if (overrides.size === 0) return undefined
|
|
172
|
+
|
|
173
|
+
const normalized = args.path.replace(/\\/g, "/")
|
|
174
|
+
if (!normalized.includes("/dashboard/dist/")) return undefined
|
|
175
|
+
|
|
176
|
+
const srcEntry = normalized
|
|
177
|
+
.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx")
|
|
178
|
+
|
|
179
|
+
let contents: string
|
|
180
|
+
try {
|
|
181
|
+
contents = fs.readFileSync(srcEntry, "utf-8")
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (process.env.NODE_ENV === "development") {
|
|
187
|
+
console.log(
|
|
188
|
+
`[custom-dashboard] Redirecting entry: ${args.path} → ${srcEntry}`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
contents,
|
|
193
|
+
loader: "tsx",
|
|
194
|
+
resolveDir: path.dirname(srcEntry),
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// 2. Intercept individual source files to swap with overrides.
|
|
199
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
200
|
+
if (overrides.size === 0) return undefined
|
|
201
|
+
|
|
202
|
+
const normalized = args.path.replace(/\\/g, "/")
|
|
203
|
+
if (!normalized.includes("/dashboard/src/")) return undefined
|
|
204
|
+
|
|
205
|
+
// Skip index/barrel files to preserve re-exports
|
|
206
|
+
const fileName = path.basename(args.path)
|
|
207
|
+
if (fileName.startsWith("index.")) return undefined
|
|
208
|
+
|
|
209
|
+
const componentName = getComponentName(args.path)
|
|
210
|
+
if (componentName && overrides.has(componentName)) {
|
|
211
|
+
const overridePath = overrides.get(componentName)!
|
|
212
|
+
const ext = path.extname(overridePath).slice(1)
|
|
213
|
+
const loader = VALID_LOADERS[ext] || "tsx"
|
|
137
214
|
|
|
138
215
|
let contents: string
|
|
139
216
|
try {
|
|
140
|
-
contents = fs.readFileSync(
|
|
217
|
+
contents = fs.readFileSync(overridePath, "utf-8")
|
|
141
218
|
} catch {
|
|
142
219
|
return undefined
|
|
143
220
|
}
|
|
144
221
|
|
|
222
|
+
// Track this as a real applied override
|
|
223
|
+
appliedOverrides.add(componentName)
|
|
224
|
+
|
|
145
225
|
if (process.env.NODE_ENV === "development") {
|
|
146
226
|
console.log(
|
|
147
|
-
`[custom-dashboard]
|
|
227
|
+
`[custom-dashboard] Override: ${componentName} → ${overridePath}`
|
|
148
228
|
)
|
|
149
229
|
}
|
|
150
230
|
return {
|
|
151
231
|
contents,
|
|
152
|
-
loader:
|
|
153
|
-
resolveDir: path.dirname(
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
// 2. Intercept individual source files to swap with overrides.
|
|
158
|
-
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
159
|
-
const normalized = args.path.replace(/\\/g, "/")
|
|
160
|
-
if (!normalized.includes("/dashboard/src/")) return undefined
|
|
161
|
-
|
|
162
|
-
// Skip index/barrel files to preserve re-exports
|
|
163
|
-
const fileName = path.basename(args.path)
|
|
164
|
-
if (fileName.startsWith("index.")) return undefined
|
|
165
|
-
|
|
166
|
-
const componentName = getComponentName(args.path)
|
|
167
|
-
if (componentName && overrides.has(componentName)) {
|
|
168
|
-
const overridePath = overrides.get(componentName)!
|
|
169
|
-
const ext = path.extname(overridePath).slice(1)
|
|
170
|
-
const loader = VALID_LOADERS[ext] || "tsx"
|
|
171
|
-
|
|
172
|
-
let contents: string
|
|
173
|
-
try {
|
|
174
|
-
contents = fs.readFileSync(overridePath, "utf-8")
|
|
175
|
-
} catch {
|
|
176
|
-
return undefined
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (process.env.NODE_ENV === "development") {
|
|
180
|
-
console.log(
|
|
181
|
-
`[custom-dashboard] Override: ${componentName} → ${overridePath}`
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
contents,
|
|
186
|
-
loader: loader as any,
|
|
187
|
-
resolveDir: path.dirname(overridePath),
|
|
188
|
-
}
|
|
232
|
+
loader: loader as any,
|
|
233
|
+
resolveDir: path.dirname(overridePath),
|
|
189
234
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
}
|
|
235
|
+
}
|
|
236
|
+
return undefined
|
|
237
|
+
})
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Force re-optimisation so overrides are always applied
|
|
242
|
+
config.optimizeDeps.force = true
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
configureServer(server: ViteDevServer) {
|
|
246
|
+
if (!fs.existsSync(componentsDir)) return
|
|
194
247
|
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
249
|
+
|
|
250
|
+
/** Extract override-candidate name from a watched file, or null */
|
|
251
|
+
const extractName = (file: string): string | null => {
|
|
252
|
+
const normalized = file.replace(/\\/g, "/")
|
|
253
|
+
if (!normalized.startsWith(componentsDir.replace(/\\/g, "/"))) return null
|
|
254
|
+
const ext = path.extname(file)
|
|
255
|
+
if (!COMPONENT_EXT_SET.has(ext)) return null
|
|
256
|
+
const fileName = path.basename(file)
|
|
257
|
+
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
258
|
+
if (!name || name === "index") return null
|
|
259
|
+
return name
|
|
197
260
|
}
|
|
261
|
+
|
|
262
|
+
/** Re-collect overrides from disk and restart the dev server */
|
|
263
|
+
const triggerRestart = (name: string, reason: string) => {
|
|
264
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
265
|
+
debounceTimer = setTimeout(() => {
|
|
266
|
+
const newOverrides = new Map<string, string>()
|
|
267
|
+
const collectedFiles = collectComponentFiles(componentsDir).sort()
|
|
268
|
+
for (const fullPath of collectedFiles) {
|
|
269
|
+
const fn = path.basename(fullPath)
|
|
270
|
+
const n = fn.replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
271
|
+
if (n && n !== "index") {
|
|
272
|
+
newOverrides.set(n, fullPath)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
overridesByName.clear()
|
|
277
|
+
for (const [k, v] of newOverrides) overridesByName.set(k, v)
|
|
278
|
+
|
|
279
|
+
console.log(
|
|
280
|
+
`[custom-dashboard] Override "${name}" ${reason} → restarting Vite...`
|
|
281
|
+
)
|
|
282
|
+
console.log(
|
|
283
|
+
`[custom-dashboard] overrides:`,
|
|
284
|
+
[...overridesByName.keys()]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
server.restart()
|
|
288
|
+
}, 300)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
server.watcher.add(componentsDir)
|
|
292
|
+
|
|
293
|
+
// ADD: new file — restart only if its name matches a known dashboard
|
|
294
|
+
// component (i.e. it could be a new override)
|
|
295
|
+
server.watcher.on("add", (file: string) => {
|
|
296
|
+
const name = extractName(file)
|
|
297
|
+
if (name && knownDashboardComponents.has(name)) {
|
|
298
|
+
triggerRestart(name, "created")
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// CHANGE: file modified — restart only if it's an active override
|
|
303
|
+
// (was matched by esbuild during pre-bundling)
|
|
304
|
+
server.watcher.on("change", (file: string) => {
|
|
305
|
+
const name = extractName(file)
|
|
306
|
+
if (name && appliedOverrides.has(name)) {
|
|
307
|
+
triggerRestart(name, "modified")
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// UNLINK: file deleted — restart only if it was an active override
|
|
312
|
+
server.watcher.on("unlink", (file: string) => {
|
|
313
|
+
const name = extractName(file)
|
|
314
|
+
if (name && appliedOverrides.has(name)) {
|
|
315
|
+
appliedOverrides.delete(name)
|
|
316
|
+
triggerRestart(name, "deleted")
|
|
317
|
+
}
|
|
318
|
+
})
|
|
198
319
|
},
|
|
199
320
|
|
|
200
321
|
resolveId(source) {
|