@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 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 (hasOverrides && process.env.NODE_ENV === "development") {
109
- console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
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
- if (hasOverrides) {
119
- config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
120
- config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
121
- const overrides = overridesByName;
122
- config.optimizeDeps.esbuildOptions.plugins.push({
123
- name: "dashboard-component-overrides",
124
- setup(build) {
125
- build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
126
- const normalized = args.path.replace(/\\/g, "/");
127
- if (!normalized.includes("/dashboard/dist/")) return void 0;
128
- const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
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(srcEntry, "utf-8");
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] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
194
+ `[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
138
195
  );
139
196
  }
140
197
  return {
141
198
  contents,
142
- loader: "tsx",
143
- resolveDir: import_path.default.dirname(srcEntry)
199
+ loader,
200
+ resolveDir: import_path.default.dirname(overridePath)
144
201
  };
145
- });
146
- build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
147
- const normalized = args.path.replace(/\\/g, "/");
148
- if (!normalized.includes("/dashboard/src/")) return void 0;
149
- const fileName = import_path.default.basename(args.path);
150
- if (fileName.startsWith("index.")) return void 0;
151
- const componentName = getComponentName(args.path);
152
- if (componentName && overrides.has(componentName)) {
153
- const overridePath = overrides.get(componentName);
154
- const ext = import_path.default.extname(overridePath).slice(1);
155
- const loader = VALID_LOADERS[ext] || "tsx";
156
- let contents;
157
- try {
158
- contents = import_fs.default.readFileSync(overridePath, "utf-8");
159
- } catch {
160
- return void 0;
161
- }
162
- if (process.env.NODE_ENV === "development") {
163
- console.log(
164
- `[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
165
- );
166
- }
167
- return {
168
- contents,
169
- loader,
170
- resolveDir: import_path.default.dirname(overridePath)
171
- };
172
- }
173
- return void 0;
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
- config.optimizeDeps.force = true;
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 (hasOverrides && process.env.NODE_ENV === "development") {
74
- console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
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
- if (hasOverrides) {
84
- config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
85
- config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
86
- const overrides = overridesByName;
87
- config.optimizeDeps.esbuildOptions.plugins.push({
88
- name: "dashboard-component-overrides",
89
- setup(build) {
90
- build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
91
- const normalized = args.path.replace(/\\/g, "/");
92
- if (!normalized.includes("/dashboard/dist/")) return void 0;
93
- const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
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(srcEntry, "utf-8");
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] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
159
+ `[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
103
160
  );
104
161
  }
105
162
  return {
106
163
  contents,
107
- loader: "tsx",
108
- resolveDir: path.dirname(srcEntry)
164
+ loader,
165
+ resolveDir: path.dirname(overridePath)
109
166
  };
110
- });
111
- build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
112
- const normalized = args.path.replace(/\\/g, "/");
113
- if (!normalized.includes("/dashboard/src/")) return void 0;
114
- const fileName = path.basename(args.path);
115
- if (fileName.startsWith("index.")) return void 0;
116
- const componentName = getComponentName(args.path);
117
- if (componentName && overrides.has(componentName)) {
118
- const overridePath = overrides.get(componentName);
119
- const ext = path.extname(overridePath).slice(1);
120
- const loader = VALID_LOADERS[ext] || "tsx";
121
- let contents;
122
- try {
123
- contents = fs.readFileSync(overridePath, "utf-8");
124
- } catch {
125
- return void 0;
126
- }
127
- if (process.env.NODE_ENV === "development") {
128
- console.log(
129
- `[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
130
- );
131
- }
132
- return {
133
- contents,
134
- loader,
135
- resolveDir: path.dirname(overridePath)
136
- };
137
- }
138
- return void 0;
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
- config.optimizeDeps.force = true;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantajs/dashboard",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "B2B Admin Dashboard for Medusa - Fork of @medusajs/dashboard",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -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 (hasOverrides && process.env.NODE_ENV === "development") {
102
- console.log("[custom-dashboard] overrides:", [...overridesByName.keys()])
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 (hasOverrides) {
116
- // Strategy: the package.json points to dist/app.mjs (so the browser
117
- // gets a working pre-bundled chunk — no blank page). But during
118
- // esbuild pre-bundling we redirect the dist entry to the source TSX
119
- // via onLoad, so esbuild follows individual imports and we can swap
120
- // component files with the user's overrides.
121
- config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {}
122
- config.optimizeDeps.esbuildOptions.plugins =
123
- config.optimizeDeps.esbuildOptions.plugins || []
124
-
125
- const overrides = overridesByName
126
- config.optimizeDeps.esbuildOptions.plugins.push({
127
- name: "dashboard-component-overrides",
128
- setup(build) {
129
- // 1. Redirect the dist entry to source so esbuild processes
130
- // individual TSX files instead of one big pre-built bundle.
131
- build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
132
- const normalized = args.path.replace(/\\/g, "/")
133
- if (!normalized.includes("/dashboard/dist/")) return undefined
134
-
135
- const srcEntry = normalized
136
- .replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx")
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(srcEntry, "utf-8")
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] Redirecting entry: ${args.path} → ${srcEntry}`
227
+ `[custom-dashboard] Override: ${componentName} → ${overridePath}`
148
228
  )
149
229
  }
150
230
  return {
151
231
  contents,
152
- loader: "tsx",
153
- resolveDir: path.dirname(srcEntry),
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
- return undefined
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
- // Force re-optimisation so overrides are always applied
196
- config.optimizeDeps.force = true
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) {