@mantajs/dashboard 0.1.15 → 0.1.17
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 +48 -0
- package/dist/vite-plugin/index.d.mts +17 -2
- package/dist/vite-plugin/index.d.ts +17 -2
- package/dist/vite-plugin/index.js +176 -71
- package/dist/vite-plugin/index.mjs +176 -71
- package/package.json +1 -1
- package/src/vite-plugin/index.ts +263 -97
package/README.md
CHANGED
|
@@ -112,6 +112,16 @@ const OrderActivitySection = () => {
|
|
|
112
112
|
export default OrderActivitySection
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
**Developer experience (HMR):**
|
|
116
|
+
|
|
117
|
+
| Action | Behavior | Details |
|
|
118
|
+
|--------|----------|---------|
|
|
119
|
+
| **Modify** an override | **HMR** (Hot Module Replacement) | The component is swapped in-place — no page reload, no React state loss. Instant feedback. |
|
|
120
|
+
| **Create** a new override | **Automatic full reload** (~2-3s) | The Vite server restarts to rebuild the pre-bundle, then the browser reloads automatically. No manual refresh needed. |
|
|
121
|
+
| **Delete** an override | **Automatic full reload** (~2-3s) | Same as creation — the original dashboard component is restored automatically. |
|
|
122
|
+
|
|
123
|
+
Under the hood, override files are kept as **separate Vite modules** (not inlined into the pre-bundled chunk). This allows React Fast Refresh to handle modifications via standard HMR. Creation and deletion require a server restart because the esbuild chunk structure changes.
|
|
124
|
+
|
|
115
125
|
**Important notes:**
|
|
116
126
|
|
|
117
127
|
- Override files are discovered **recursively** in `src/admin/components/` and all its subdirectories.
|
|
@@ -143,6 +153,44 @@ your-project/
|
|
|
143
153
|
|
|
144
154
|
This uses Medusa's `@medusajs/admin-vite-plugin` to discover routes in `src/admin/routes/`, combined with a custom merge function that ensures backward compatibility.
|
|
145
155
|
|
|
156
|
+
**Route page example:**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/admin/routes/orders/page.tsx
|
|
160
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
|
161
|
+
import { ShoppingCart } from "@medusajs/icons"
|
|
162
|
+
import { Container, Heading } from "@medusajs/ui"
|
|
163
|
+
|
|
164
|
+
const OrdersPage = () => {
|
|
165
|
+
return (
|
|
166
|
+
<Container>
|
|
167
|
+
<Heading level="h1">My Custom Orders Page</Heading>
|
|
168
|
+
{/* Your custom orders list */}
|
|
169
|
+
</Container>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const config = defineRouteConfig({
|
|
174
|
+
label: "Orders",
|
|
175
|
+
icon: ShoppingCart,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
export default OrdersPage
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Developer experience:**
|
|
182
|
+
|
|
183
|
+
Route overrides use Medusa's standard admin extension system (`@medusajs/admin-vite-plugin`), which provides its own HMR. Modifying a route page triggers a standard Vite HMR update — the page refreshes instantly without a full reload.
|
|
184
|
+
|
|
185
|
+
**Component overrides vs Route overrides — when to use which:**
|
|
186
|
+
|
|
187
|
+
| Use case | Approach |
|
|
188
|
+
|----------|----------|
|
|
189
|
+
| Replace a **full page** (e.g., the orders list) | Route override (`src/admin/routes/orders/page.tsx`) |
|
|
190
|
+
| Replace a **section** inside a page (e.g., the customer info block in order detail) | Component override (`src/admin/components/order-customer-section.tsx`) |
|
|
191
|
+
| Add a **new page** that doesn't exist in the dashboard | Route override (`src/admin/routes/my-page/page.tsx`) |
|
|
192
|
+
| Tweak a **reusable UI element** used across multiple pages | Component override |
|
|
193
|
+
|
|
146
194
|
### 3. Custom Menu Configuration
|
|
147
195
|
|
|
148
196
|
Define your own sidebar menu by creating a `src/admin/menu.config.tsx` file:
|
|
@@ -22,8 +22,23 @@ type MenuConfig = {
|
|
|
22
22
|
*
|
|
23
23
|
* Handles:
|
|
24
24
|
* 1. Menu config virtual module (virtual:dashboard/menu-config)
|
|
25
|
-
* 2. Component overrides — any file in src/admin/components/
|
|
26
|
-
* dashboard component
|
|
25
|
+
* 2. Component overrides — any file in src/admin/components/ whose name
|
|
26
|
+
* matches a dashboard component replaces it at build time.
|
|
27
|
+
*
|
|
28
|
+
* HMR Architecture:
|
|
29
|
+
* - Override files are NOT inlined into the esbuild pre-bundled chunk.
|
|
30
|
+
* Instead, the chunk contains `export * from "/@fs/path/to/override.tsx"`,
|
|
31
|
+
* keeping the override as a **separate Vite module**.
|
|
32
|
+
* - Because the override is a separate module exporting React components,
|
|
33
|
+
* @vitejs/plugin-react adds React Fast Refresh boundaries
|
|
34
|
+
* (import.meta.hot.accept). This makes the module self-accepting for HMR.
|
|
35
|
+
* - On MODIFICATION: Vite detects the file change, transforms it, and sends
|
|
36
|
+
* an HMR update. React Fast Refresh swaps the component — no page reload.
|
|
37
|
+
* - On CREATION or DELETION: The esbuild pre-bundle must be rebuilt (the
|
|
38
|
+
* chunk structure changes). We restart Vite + send a full-reload to the
|
|
39
|
+
* browser so it picks up the new chunks automatically.
|
|
40
|
+
* - The fs.watch is independent from Vite's internal watcher, so it
|
|
41
|
+
* survives server.restart() calls without losing events.
|
|
27
42
|
*/
|
|
28
43
|
declare function customDashboardPlugin(): Plugin;
|
|
29
44
|
declare const menuConfigPlugin: typeof customDashboardPlugin;
|
|
@@ -22,8 +22,23 @@ type MenuConfig = {
|
|
|
22
22
|
*
|
|
23
23
|
* Handles:
|
|
24
24
|
* 1. Menu config virtual module (virtual:dashboard/menu-config)
|
|
25
|
-
* 2. Component overrides — any file in src/admin/components/
|
|
26
|
-
* dashboard component
|
|
25
|
+
* 2. Component overrides — any file in src/admin/components/ whose name
|
|
26
|
+
* matches a dashboard component replaces it at build time.
|
|
27
|
+
*
|
|
28
|
+
* HMR Architecture:
|
|
29
|
+
* - Override files are NOT inlined into the esbuild pre-bundled chunk.
|
|
30
|
+
* Instead, the chunk contains `export * from "/@fs/path/to/override.tsx"`,
|
|
31
|
+
* keeping the override as a **separate Vite module**.
|
|
32
|
+
* - Because the override is a separate module exporting React components,
|
|
33
|
+
* @vitejs/plugin-react adds React Fast Refresh boundaries
|
|
34
|
+
* (import.meta.hot.accept). This makes the module self-accepting for HMR.
|
|
35
|
+
* - On MODIFICATION: Vite detects the file change, transforms it, and sends
|
|
36
|
+
* an HMR update. React Fast Refresh swaps the component — no page reload.
|
|
37
|
+
* - On CREATION or DELETION: The esbuild pre-bundle must be rebuilt (the
|
|
38
|
+
* chunk structure changes). We restart Vite + send a full-reload to the
|
|
39
|
+
* browser so it picks up the new chunks automatically.
|
|
40
|
+
* - The fs.watch is independent from Vite's internal watcher, so it
|
|
41
|
+
* survives server.restart() calls without losing events.
|
|
27
42
|
*/
|
|
28
43
|
declare function customDashboardPlugin(): Plugin;
|
|
29
44
|
declare const menuConfigPlugin: typeof customDashboardPlugin;
|
|
@@ -38,16 +38,9 @@ var import_path = __toESM(require("path"));
|
|
|
38
38
|
var import_fs = __toESM(require("fs"));
|
|
39
39
|
var MENU_VIRTUAL_ID = "virtual:dashboard/menu-config";
|
|
40
40
|
var MENU_RESOLVED_ID = "\0" + MENU_VIRTUAL_ID;
|
|
41
|
+
var OVERRIDE_PREFIX = "__mantajs_override__:";
|
|
41
42
|
var COMPONENT_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mts", ".mjs"];
|
|
42
43
|
var COMPONENT_EXT_SET = new Set(COMPONENT_EXTENSIONS);
|
|
43
|
-
var VALID_LOADERS = {
|
|
44
|
-
tsx: "tsx",
|
|
45
|
-
ts: "ts",
|
|
46
|
-
jsx: "jsx",
|
|
47
|
-
js: "js",
|
|
48
|
-
mts: "ts",
|
|
49
|
-
mjs: "js"
|
|
50
|
-
};
|
|
51
44
|
function collectComponentFiles(dir, depth = 0) {
|
|
52
45
|
if (depth > 20) return [];
|
|
53
46
|
const results = [];
|
|
@@ -86,27 +79,50 @@ function getComponentName(filePath) {
|
|
|
86
79
|
}
|
|
87
80
|
return baseName;
|
|
88
81
|
}
|
|
82
|
+
function findDashboardSrc() {
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
const candidates = [
|
|
85
|
+
import_path.default.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
86
|
+
import_path.default.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
87
|
+
import_path.default.join(cwd, ".yalc", "@mantajs", "dashboard", "src")
|
|
88
|
+
];
|
|
89
|
+
for (const dir of candidates) {
|
|
90
|
+
if (import_fs.default.existsSync(dir)) return dir;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
89
94
|
function customDashboardPlugin() {
|
|
90
95
|
const componentsDir = import_path.default.resolve(process.cwd(), "src/admin/components");
|
|
91
96
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
97
|
+
const dashboardComponents = /* @__PURE__ */ new Set();
|
|
98
|
+
const dashboardSrc = findDashboardSrc();
|
|
99
|
+
if (dashboardSrc) {
|
|
100
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
101
|
+
const cName = getComponentName(f);
|
|
102
|
+
if (cName) dashboardComponents.add(cName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
92
105
|
if (import_fs.default.existsSync(componentsDir)) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const fileName = import_path.default.basename(fullPath);
|
|
96
|
-
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
106
|
+
for (const fullPath of collectComponentFiles(componentsDir).sort()) {
|
|
107
|
+
const name = import_path.default.basename(fullPath).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
97
108
|
if (name && name !== "index") {
|
|
98
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
99
|
-
console.warn(
|
|
100
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
109
|
overridesByName.set(name, fullPath);
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
const hasOverrides = overridesByName.size > 0;
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
let currentServer = null;
|
|
115
|
+
let watcherCreated = false;
|
|
116
|
+
const knownOverrideFiles = new Set(overridesByName.values());
|
|
117
|
+
if (process.env.NODE_ENV === "development") {
|
|
118
|
+
if (hasOverrides) {
|
|
119
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
120
|
+
}
|
|
121
|
+
if (dashboardComponents.size > 0) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
return {
|
|
112
128
|
name: "custom-dashboard",
|
|
@@ -115,70 +131,159 @@ function customDashboardPlugin() {
|
|
|
115
131
|
config.optimizeDeps = config.optimizeDeps || {};
|
|
116
132
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || [];
|
|
117
133
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
|
|
135
|
+
config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
|
|
136
|
+
const overrides = overridesByName;
|
|
137
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
138
|
+
name: "dashboard-component-overrides",
|
|
139
|
+
setup(build) {
|
|
140
|
+
build.onResolve({ filter: /^__mantajs_override__:/ }, (args) => ({
|
|
141
|
+
path: args.path,
|
|
142
|
+
external: true
|
|
143
|
+
}));
|
|
144
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
145
|
+
if (overrides.size === 0) return void 0;
|
|
146
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
147
|
+
if (!normalized.includes("/dashboard/dist/")) return void 0;
|
|
148
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
|
|
149
|
+
let contents;
|
|
150
|
+
try {
|
|
151
|
+
contents = import_fs.default.readFileSync(srcEntry, "utf-8");
|
|
152
|
+
} catch {
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
if (process.env.NODE_ENV === "development") {
|
|
156
|
+
console.log(`[custom-dashboard] Redirecting entry \u2192 ${srcEntry}`);
|
|
157
|
+
}
|
|
158
|
+
return { contents, loader: "tsx", resolveDir: import_path.default.dirname(srcEntry) };
|
|
159
|
+
});
|
|
160
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
161
|
+
if (overrides.size === 0) return void 0;
|
|
162
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
163
|
+
if (!normalized.includes("/dashboard/src/")) return void 0;
|
|
164
|
+
const fileName = import_path.default.basename(args.path);
|
|
165
|
+
if (fileName.startsWith("index.")) return void 0;
|
|
166
|
+
const componentName = getComponentName(args.path);
|
|
167
|
+
if (componentName && overrides.has(componentName)) {
|
|
168
|
+
const overridePath = overrides.get(componentName);
|
|
169
|
+
const normalizedPath = overridePath.replace(/\\/g, "/");
|
|
135
170
|
if (process.env.NODE_ENV === "development") {
|
|
136
|
-
console.log(
|
|
137
|
-
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
138
|
-
);
|
|
171
|
+
console.log(`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`);
|
|
139
172
|
}
|
|
140
173
|
return {
|
|
141
|
-
contents
|
|
174
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
142
175
|
loader: "tsx",
|
|
143
|
-
resolveDir: import_path.default.dirname(
|
|
176
|
+
resolveDir: import_path.default.dirname(args.path)
|
|
144
177
|
};
|
|
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
|
-
|
|
178
|
+
}
|
|
179
|
+
return void 0;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
config.optimizeDeps.esbuildOptions.define = {
|
|
184
|
+
...config.optimizeDeps.esbuildOptions.define,
|
|
185
|
+
"__MANTAJS_OVERRIDES__": JSON.stringify(
|
|
186
|
+
[...overrides.keys()].sort().join(",")
|
|
187
|
+
)
|
|
188
|
+
};
|
|
189
|
+
config.optimizeDeps.force = true;
|
|
190
|
+
},
|
|
191
|
+
configureServer(server) {
|
|
192
|
+
currentServer = server;
|
|
193
|
+
if (!import_fs.default.existsSync(componentsDir)) return;
|
|
194
|
+
if (!watcherCreated) {
|
|
195
|
+
watcherCreated = true;
|
|
196
|
+
let debounceTimer = null;
|
|
197
|
+
import_fs.default.watch(componentsDir, { recursive: true }, (_event, filename) => {
|
|
198
|
+
if (!filename) return;
|
|
199
|
+
const ext = import_path.default.extname(filename);
|
|
200
|
+
if (!COMPONENT_EXT_SET.has(ext)) return;
|
|
201
|
+
const name = import_path.default.basename(filename).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
202
|
+
if (!name || name === "index") return;
|
|
203
|
+
if (!dashboardComponents.has(name)) return;
|
|
204
|
+
const fullPath = import_path.default.resolve(componentsDir, filename);
|
|
205
|
+
const fileExists = import_fs.default.existsSync(fullPath);
|
|
206
|
+
const wasKnown = knownOverrideFiles.has(fullPath);
|
|
207
|
+
if (fileExists && wasKnown) {
|
|
208
|
+
const mods = currentServer?.moduleGraph.getModulesByFile(fullPath);
|
|
209
|
+
if (mods && mods.size > 0) {
|
|
210
|
+
for (const mod of mods) {
|
|
211
|
+
currentServer.moduleGraph.invalidateModule(mod);
|
|
172
212
|
}
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
currentServer.ws.send({
|
|
214
|
+
type: "update",
|
|
215
|
+
updates: [...mods].map((mod) => ({
|
|
216
|
+
type: "js-update",
|
|
217
|
+
path: mod.url,
|
|
218
|
+
acceptedPath: mod.url,
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
explicitImportRequired: false
|
|
221
|
+
}))
|
|
222
|
+
});
|
|
223
|
+
console.log(`[custom-dashboard] Override "${name}" modified \u2192 HMR`);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(`[custom-dashboard] Override "${name}" not in graph \u2192 force-reload`);
|
|
226
|
+
currentServer?.ws.send({ type: "custom", event: "mantajs:force-reload" });
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
175
229
|
}
|
|
230
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
231
|
+
debounceTimer = setTimeout(async () => {
|
|
232
|
+
overridesByName.clear();
|
|
233
|
+
knownOverrideFiles.clear();
|
|
234
|
+
for (const fp of collectComponentFiles(componentsDir).sort()) {
|
|
235
|
+
const n = import_path.default.basename(fp).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
236
|
+
if (n && n !== "index") {
|
|
237
|
+
overridesByName.set(n, fp);
|
|
238
|
+
knownOverrideFiles.add(fp);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const action = fileExists ? "created" : "deleted";
|
|
242
|
+
console.log(`[custom-dashboard] Override "${name}" ${action} \u2192 restarting...`);
|
|
243
|
+
console.log(`[custom-dashboard] overrides:`, [...overridesByName.keys()]);
|
|
244
|
+
try {
|
|
245
|
+
if (!currentServer) {
|
|
246
|
+
console.warn(`[custom-dashboard] No server available for restart`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await currentServer.restart();
|
|
250
|
+
currentServer.ws.send({
|
|
251
|
+
type: "custom",
|
|
252
|
+
event: "mantajs:force-reload"
|
|
253
|
+
});
|
|
254
|
+
console.log(`[custom-dashboard] Force-reload sent to browser`);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.error(`[custom-dashboard] Restart failed:`, e);
|
|
257
|
+
}
|
|
258
|
+
}, 300);
|
|
176
259
|
});
|
|
177
|
-
config.optimizeDeps.force = true;
|
|
178
260
|
}
|
|
179
261
|
},
|
|
262
|
+
handleHotUpdate({ file }) {
|
|
263
|
+
if (knownOverrideFiles.has(file)) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
transformIndexHtml(html) {
|
|
268
|
+
return html.replace(
|
|
269
|
+
"</head>",
|
|
270
|
+
`<script type="module">
|
|
271
|
+
if (import.meta.hot) {
|
|
272
|
+
import.meta.hot.on("mantajs:force-reload", () => {
|
|
273
|
+
const url = new URL(location.href);
|
|
274
|
+
url.searchParams.set("_r", Date.now().toString());
|
|
275
|
+
location.replace(url.href);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
</head>`
|
|
280
|
+
);
|
|
281
|
+
},
|
|
180
282
|
resolveId(source) {
|
|
181
283
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
|
284
|
+
if (source.startsWith(OVERRIDE_PREFIX)) {
|
|
285
|
+
return source.slice(OVERRIDE_PREFIX.length);
|
|
286
|
+
}
|
|
182
287
|
return null;
|
|
183
288
|
},
|
|
184
289
|
load(id) {
|
|
@@ -3,16 +3,9 @@ import path from "path";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
var MENU_VIRTUAL_ID = "virtual:dashboard/menu-config";
|
|
5
5
|
var MENU_RESOLVED_ID = "\0" + MENU_VIRTUAL_ID;
|
|
6
|
+
var OVERRIDE_PREFIX = "__mantajs_override__:";
|
|
6
7
|
var COMPONENT_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mts", ".mjs"];
|
|
7
8
|
var COMPONENT_EXT_SET = new Set(COMPONENT_EXTENSIONS);
|
|
8
|
-
var VALID_LOADERS = {
|
|
9
|
-
tsx: "tsx",
|
|
10
|
-
ts: "ts",
|
|
11
|
-
jsx: "jsx",
|
|
12
|
-
js: "js",
|
|
13
|
-
mts: "ts",
|
|
14
|
-
mjs: "js"
|
|
15
|
-
};
|
|
16
9
|
function collectComponentFiles(dir, depth = 0) {
|
|
17
10
|
if (depth > 20) return [];
|
|
18
11
|
const results = [];
|
|
@@ -51,27 +44,50 @@ function getComponentName(filePath) {
|
|
|
51
44
|
}
|
|
52
45
|
return baseName;
|
|
53
46
|
}
|
|
47
|
+
function findDashboardSrc() {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
51
|
+
path.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
52
|
+
path.join(cwd, ".yalc", "@mantajs", "dashboard", "src")
|
|
53
|
+
];
|
|
54
|
+
for (const dir of candidates) {
|
|
55
|
+
if (fs.existsSync(dir)) return dir;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
54
59
|
function customDashboardPlugin() {
|
|
55
60
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components");
|
|
56
61
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
62
|
+
const dashboardComponents = /* @__PURE__ */ new Set();
|
|
63
|
+
const dashboardSrc = findDashboardSrc();
|
|
64
|
+
if (dashboardSrc) {
|
|
65
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
66
|
+
const cName = getComponentName(f);
|
|
67
|
+
if (cName) dashboardComponents.add(cName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
57
70
|
if (fs.existsSync(componentsDir)) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const fileName = path.basename(fullPath);
|
|
61
|
-
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
71
|
+
for (const fullPath of collectComponentFiles(componentsDir).sort()) {
|
|
72
|
+
const name = path.basename(fullPath).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
62
73
|
if (name && name !== "index") {
|
|
63
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
64
|
-
console.warn(
|
|
65
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
74
|
overridesByName.set(name, fullPath);
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
const hasOverrides = overridesByName.size > 0;
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
let currentServer = null;
|
|
80
|
+
let watcherCreated = false;
|
|
81
|
+
const knownOverrideFiles = new Set(overridesByName.values());
|
|
82
|
+
if (process.env.NODE_ENV === "development") {
|
|
83
|
+
if (hasOverrides) {
|
|
84
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
85
|
+
}
|
|
86
|
+
if (dashboardComponents.size > 0) {
|
|
87
|
+
console.log(
|
|
88
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
92
|
return {
|
|
77
93
|
name: "custom-dashboard",
|
|
@@ -80,70 +96,159 @@ function customDashboardPlugin() {
|
|
|
80
96
|
config.optimizeDeps = config.optimizeDeps || {};
|
|
81
97
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || [];
|
|
82
98
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
|
|
100
|
+
config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
|
|
101
|
+
const overrides = overridesByName;
|
|
102
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
103
|
+
name: "dashboard-component-overrides",
|
|
104
|
+
setup(build) {
|
|
105
|
+
build.onResolve({ filter: /^__mantajs_override__:/ }, (args) => ({
|
|
106
|
+
path: args.path,
|
|
107
|
+
external: true
|
|
108
|
+
}));
|
|
109
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
110
|
+
if (overrides.size === 0) return void 0;
|
|
111
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
112
|
+
if (!normalized.includes("/dashboard/dist/")) return void 0;
|
|
113
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx");
|
|
114
|
+
let contents;
|
|
115
|
+
try {
|
|
116
|
+
contents = fs.readFileSync(srcEntry, "utf-8");
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
if (process.env.NODE_ENV === "development") {
|
|
121
|
+
console.log(`[custom-dashboard] Redirecting entry \u2192 ${srcEntry}`);
|
|
122
|
+
}
|
|
123
|
+
return { contents, loader: "tsx", resolveDir: path.dirname(srcEntry) };
|
|
124
|
+
});
|
|
125
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
126
|
+
if (overrides.size === 0) return void 0;
|
|
127
|
+
const normalized = args.path.replace(/\\/g, "/");
|
|
128
|
+
if (!normalized.includes("/dashboard/src/")) return void 0;
|
|
129
|
+
const fileName = path.basename(args.path);
|
|
130
|
+
if (fileName.startsWith("index.")) return void 0;
|
|
131
|
+
const componentName = getComponentName(args.path);
|
|
132
|
+
if (componentName && overrides.has(componentName)) {
|
|
133
|
+
const overridePath = overrides.get(componentName);
|
|
134
|
+
const normalizedPath = overridePath.replace(/\\/g, "/");
|
|
100
135
|
if (process.env.NODE_ENV === "development") {
|
|
101
|
-
console.log(
|
|
102
|
-
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
103
|
-
);
|
|
136
|
+
console.log(`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`);
|
|
104
137
|
}
|
|
105
138
|
return {
|
|
106
|
-
contents
|
|
139
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
107
140
|
loader: "tsx",
|
|
108
|
-
resolveDir: path.dirname(
|
|
141
|
+
resolveDir: path.dirname(args.path)
|
|
109
142
|
};
|
|
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
|
-
|
|
143
|
+
}
|
|
144
|
+
return void 0;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
config.optimizeDeps.esbuildOptions.define = {
|
|
149
|
+
...config.optimizeDeps.esbuildOptions.define,
|
|
150
|
+
"__MANTAJS_OVERRIDES__": JSON.stringify(
|
|
151
|
+
[...overrides.keys()].sort().join(",")
|
|
152
|
+
)
|
|
153
|
+
};
|
|
154
|
+
config.optimizeDeps.force = true;
|
|
155
|
+
},
|
|
156
|
+
configureServer(server) {
|
|
157
|
+
currentServer = server;
|
|
158
|
+
if (!fs.existsSync(componentsDir)) return;
|
|
159
|
+
if (!watcherCreated) {
|
|
160
|
+
watcherCreated = true;
|
|
161
|
+
let debounceTimer = null;
|
|
162
|
+
fs.watch(componentsDir, { recursive: true }, (_event, filename) => {
|
|
163
|
+
if (!filename) return;
|
|
164
|
+
const ext = path.extname(filename);
|
|
165
|
+
if (!COMPONENT_EXT_SET.has(ext)) return;
|
|
166
|
+
const name = path.basename(filename).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
167
|
+
if (!name || name === "index") return;
|
|
168
|
+
if (!dashboardComponents.has(name)) return;
|
|
169
|
+
const fullPath = path.resolve(componentsDir, filename);
|
|
170
|
+
const fileExists = fs.existsSync(fullPath);
|
|
171
|
+
const wasKnown = knownOverrideFiles.has(fullPath);
|
|
172
|
+
if (fileExists && wasKnown) {
|
|
173
|
+
const mods = currentServer?.moduleGraph.getModulesByFile(fullPath);
|
|
174
|
+
if (mods && mods.size > 0) {
|
|
175
|
+
for (const mod of mods) {
|
|
176
|
+
currentServer.moduleGraph.invalidateModule(mod);
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
|
|
178
|
+
currentServer.ws.send({
|
|
179
|
+
type: "update",
|
|
180
|
+
updates: [...mods].map((mod) => ({
|
|
181
|
+
type: "js-update",
|
|
182
|
+
path: mod.url,
|
|
183
|
+
acceptedPath: mod.url,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
explicitImportRequired: false
|
|
186
|
+
}))
|
|
187
|
+
});
|
|
188
|
+
console.log(`[custom-dashboard] Override "${name}" modified \u2192 HMR`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`[custom-dashboard] Override "${name}" not in graph \u2192 force-reload`);
|
|
191
|
+
currentServer?.ws.send({ type: "custom", event: "mantajs:force-reload" });
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
140
194
|
}
|
|
195
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
196
|
+
debounceTimer = setTimeout(async () => {
|
|
197
|
+
overridesByName.clear();
|
|
198
|
+
knownOverrideFiles.clear();
|
|
199
|
+
for (const fp of collectComponentFiles(componentsDir).sort()) {
|
|
200
|
+
const n = path.basename(fp).replace(/\.(tsx?|jsx?|mts|mjs)$/, "");
|
|
201
|
+
if (n && n !== "index") {
|
|
202
|
+
overridesByName.set(n, fp);
|
|
203
|
+
knownOverrideFiles.add(fp);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const action = fileExists ? "created" : "deleted";
|
|
207
|
+
console.log(`[custom-dashboard] Override "${name}" ${action} \u2192 restarting...`);
|
|
208
|
+
console.log(`[custom-dashboard] overrides:`, [...overridesByName.keys()]);
|
|
209
|
+
try {
|
|
210
|
+
if (!currentServer) {
|
|
211
|
+
console.warn(`[custom-dashboard] No server available for restart`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await currentServer.restart();
|
|
215
|
+
currentServer.ws.send({
|
|
216
|
+
type: "custom",
|
|
217
|
+
event: "mantajs:force-reload"
|
|
218
|
+
});
|
|
219
|
+
console.log(`[custom-dashboard] Force-reload sent to browser`);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(`[custom-dashboard] Restart failed:`, e);
|
|
222
|
+
}
|
|
223
|
+
}, 300);
|
|
141
224
|
});
|
|
142
|
-
config.optimizeDeps.force = true;
|
|
143
225
|
}
|
|
144
226
|
},
|
|
227
|
+
handleHotUpdate({ file }) {
|
|
228
|
+
if (knownOverrideFiles.has(file)) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
transformIndexHtml(html) {
|
|
233
|
+
return html.replace(
|
|
234
|
+
"</head>",
|
|
235
|
+
`<script type="module">
|
|
236
|
+
if (import.meta.hot) {
|
|
237
|
+
import.meta.hot.on("mantajs:force-reload", () => {
|
|
238
|
+
const url = new URL(location.href);
|
|
239
|
+
url.searchParams.set("_r", Date.now().toString());
|
|
240
|
+
location.replace(url.href);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
</script>
|
|
244
|
+
</head>`
|
|
245
|
+
);
|
|
246
|
+
},
|
|
145
247
|
resolveId(source) {
|
|
146
248
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
|
249
|
+
if (source.startsWith(OVERRIDE_PREFIX)) {
|
|
250
|
+
return source.slice(OVERRIDE_PREFIX.length);
|
|
251
|
+
}
|
|
147
252
|
return null;
|
|
148
253
|
},
|
|
149
254
|
load(id) {
|
package/package.json
CHANGED
package/src/vite-plugin/index.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
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
|
|
|
5
5
|
const MENU_VIRTUAL_ID = "virtual:dashboard/menu-config"
|
|
6
6
|
const MENU_RESOLVED_ID = "\0" + MENU_VIRTUAL_ID
|
|
7
7
|
|
|
8
|
+
// Unique prefix for override imports — esbuild marks these as external,
|
|
9
|
+
// then Vite's resolveId resolves them to the actual file paths.
|
|
10
|
+
const OVERRIDE_PREFIX = "__mantajs_override__:"
|
|
11
|
+
|
|
8
12
|
const COMPONENT_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mts", ".mjs"]
|
|
9
13
|
const COMPONENT_EXT_SET = new Set(COMPONENT_EXTENSIONS)
|
|
10
14
|
|
|
11
|
-
const VALID_LOADERS: Record<string, string> = {
|
|
12
|
-
tsx: "tsx",
|
|
13
|
-
ts: "ts",
|
|
14
|
-
jsx: "jsx",
|
|
15
|
-
js: "js",
|
|
16
|
-
mts: "ts",
|
|
17
|
-
mjs: "js",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* Recursively collect all component files from a directory tree.
|
|
22
17
|
* Includes a depth guard to prevent symlink loops and skips hidden
|
|
@@ -68,29 +63,66 @@ function getComponentName(filePath: string): string | null {
|
|
|
68
63
|
return baseName
|
|
69
64
|
}
|
|
70
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Find the dashboard source directory by checking known install paths.
|
|
68
|
+
* Works with yarn resolutions, direct installs, and yalc links.
|
|
69
|
+
*/
|
|
70
|
+
function findDashboardSrc(): string | null {
|
|
71
|
+
const cwd = process.cwd()
|
|
72
|
+
const candidates = [
|
|
73
|
+
path.join(cwd, "node_modules", "@medusajs", "dashboard", "src"),
|
|
74
|
+
path.join(cwd, "node_modules", "@mantajs", "dashboard", "src"),
|
|
75
|
+
path.join(cwd, ".yalc", "@mantajs", "dashboard", "src"),
|
|
76
|
+
]
|
|
77
|
+
for (const dir of candidates) {
|
|
78
|
+
if (fs.existsSync(dir)) return dir
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
/**
|
|
72
84
|
* Unified Vite plugin for @mantajs/dashboard.
|
|
73
85
|
*
|
|
74
86
|
* Handles:
|
|
75
87
|
* 1. Menu config virtual module (virtual:dashboard/menu-config)
|
|
76
|
-
* 2. Component overrides — any file in src/admin/components/
|
|
77
|
-
* dashboard component
|
|
88
|
+
* 2. Component overrides — any file in src/admin/components/ whose name
|
|
89
|
+
* matches a dashboard component replaces it at build time.
|
|
90
|
+
*
|
|
91
|
+
* HMR Architecture:
|
|
92
|
+
* - Override files are NOT inlined into the esbuild pre-bundled chunk.
|
|
93
|
+
* Instead, the chunk contains `export * from "/@fs/path/to/override.tsx"`,
|
|
94
|
+
* keeping the override as a **separate Vite module**.
|
|
95
|
+
* - Because the override is a separate module exporting React components,
|
|
96
|
+
* @vitejs/plugin-react adds React Fast Refresh boundaries
|
|
97
|
+
* (import.meta.hot.accept). This makes the module self-accepting for HMR.
|
|
98
|
+
* - On MODIFICATION: Vite detects the file change, transforms it, and sends
|
|
99
|
+
* an HMR update. React Fast Refresh swaps the component — no page reload.
|
|
100
|
+
* - On CREATION or DELETION: The esbuild pre-bundle must be rebuilt (the
|
|
101
|
+
* chunk structure changes). We restart Vite + send a full-reload to the
|
|
102
|
+
* browser so it picks up the new chunks automatically.
|
|
103
|
+
* - The fs.watch is independent from Vite's internal watcher, so it
|
|
104
|
+
* survives server.restart() calls without losing events.
|
|
78
105
|
*/
|
|
79
106
|
export function customDashboardPlugin(): Plugin {
|
|
80
107
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components")
|
|
81
108
|
const overridesByName = new Map<string, string>()
|
|
82
109
|
|
|
110
|
+
// Scan dashboard source once at startup — used to decide if a changed
|
|
111
|
+
// file is a potential override (~966 names, ~30 KB).
|
|
112
|
+
const dashboardComponents = new Set<string>()
|
|
113
|
+
const dashboardSrc = findDashboardSrc()
|
|
114
|
+
if (dashboardSrc) {
|
|
115
|
+
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
116
|
+
const cName = getComponentName(f)
|
|
117
|
+
if (cName) dashboardComponents.add(cName)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Collect initial overrides from disk
|
|
83
122
|
if (fs.existsSync(componentsDir)) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const fileName = path.basename(fullPath)
|
|
87
|
-
const name = fileName.replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
123
|
+
for (const fullPath of collectComponentFiles(componentsDir).sort()) {
|
|
124
|
+
const name = path.basename(fullPath).replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
88
125
|
if (name && name !== "index") {
|
|
89
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
90
|
-
console.warn(
|
|
91
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
126
|
overridesByName.set(name, fullPath)
|
|
95
127
|
}
|
|
96
128
|
}
|
|
@@ -98,8 +130,22 @@ export function customDashboardPlugin(): Plugin {
|
|
|
98
130
|
|
|
99
131
|
const hasOverrides = overridesByName.size > 0
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
// Mutable ref to the latest Vite server — updated on each configureServer
|
|
134
|
+
let currentServer: ViteDevServer | null = null
|
|
135
|
+
let watcherCreated = false
|
|
136
|
+
|
|
137
|
+
// Track known override file paths to distinguish modify vs create/delete
|
|
138
|
+
const knownOverrideFiles = new Set<string>(overridesByName.values())
|
|
139
|
+
|
|
140
|
+
if (process.env.NODE_ENV === "development") {
|
|
141
|
+
if (hasOverrides) {
|
|
142
|
+
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()])
|
|
143
|
+
}
|
|
144
|
+
if (dashboardComponents.size > 0) {
|
|
145
|
+
console.log(
|
|
146
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
103
149
|
}
|
|
104
150
|
|
|
105
151
|
return {
|
|
@@ -107,98 +153,218 @@ export function customDashboardPlugin(): Plugin {
|
|
|
107
153
|
enforce: "pre",
|
|
108
154
|
|
|
109
155
|
config(config) {
|
|
110
|
-
// Always exclude the menu virtual module
|
|
111
156
|
config.optimizeDeps = config.optimizeDeps || {}
|
|
112
157
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || []
|
|
113
158
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID)
|
|
114
159
|
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {}
|
|
161
|
+
config.optimizeDeps.esbuildOptions.plugins =
|
|
162
|
+
config.optimizeDeps.esbuildOptions.plugins || []
|
|
163
|
+
|
|
164
|
+
const overrides = overridesByName
|
|
165
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
166
|
+
name: "dashboard-component-overrides",
|
|
167
|
+
setup(build) {
|
|
168
|
+
// Mark override imports as external — this keeps override files as
|
|
169
|
+
// separate ES modules that Vite processes individually, enabling
|
|
170
|
+
// React Fast Refresh HMR instead of requiring a full page reload.
|
|
171
|
+
build.onResolve({ filter: /^__mantajs_override__:/ }, (args) => ({
|
|
172
|
+
path: args.path,
|
|
173
|
+
external: true,
|
|
174
|
+
}))
|
|
175
|
+
|
|
176
|
+
// Redirect dist entry → source so esbuild processes individual files
|
|
177
|
+
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
178
|
+
if (overrides.size === 0) return undefined
|
|
179
|
+
const normalized = args.path.replace(/\\/g, "/")
|
|
180
|
+
if (!normalized.includes("/dashboard/dist/")) return undefined
|
|
181
|
+
|
|
182
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx")
|
|
183
|
+
let contents: string
|
|
184
|
+
try {
|
|
185
|
+
contents = fs.readFileSync(srcEntry, "utf-8")
|
|
186
|
+
} catch {
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (process.env.NODE_ENV === "development") {
|
|
191
|
+
console.log(`[custom-dashboard] Redirecting entry → ${srcEntry}`)
|
|
192
|
+
}
|
|
193
|
+
return { contents, loader: "tsx", resolveDir: path.dirname(srcEntry) }
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// For overridden components, emit a re-export from /@fs/ instead of
|
|
197
|
+
// inlining the file contents. The override becomes a separate Vite
|
|
198
|
+
// module with full HMR support via React Fast Refresh.
|
|
199
|
+
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
200
|
+
if (overrides.size === 0) return undefined
|
|
201
|
+
const normalized = args.path.replace(/\\/g, "/")
|
|
202
|
+
if (!normalized.includes("/dashboard/src/")) return undefined
|
|
203
|
+
|
|
204
|
+
const fileName = path.basename(args.path)
|
|
205
|
+
if (fileName.startsWith("index.")) return undefined
|
|
206
|
+
|
|
207
|
+
const componentName = getComponentName(args.path)
|
|
208
|
+
if (componentName && overrides.has(componentName)) {
|
|
209
|
+
const overridePath = overrides.get(componentName)!
|
|
210
|
+
const normalizedPath = overridePath.replace(/\\/g, "/")
|
|
144
211
|
|
|
145
212
|
if (process.env.NODE_ENV === "development") {
|
|
146
|
-
console.log(
|
|
147
|
-
`[custom-dashboard] Redirecting entry: ${args.path} → ${srcEntry}`
|
|
148
|
-
)
|
|
213
|
+
console.log(`[custom-dashboard] Override: ${componentName} → ${overridePath}`)
|
|
149
214
|
}
|
|
150
215
|
return {
|
|
151
|
-
contents
|
|
216
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
152
217
|
loader: "tsx",
|
|
153
|
-
resolveDir: path.dirname(
|
|
218
|
+
resolveDir: path.dirname(args.path),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Include override state in esbuild define — this changes Vite's dep
|
|
227
|
+
// optimization hash (?v=xxx), forcing the browser to fetch fresh chunks
|
|
228
|
+
// whenever overrides are added or removed (prevents stale cache 404s).
|
|
229
|
+
config.optimizeDeps.esbuildOptions.define = {
|
|
230
|
+
...config.optimizeDeps.esbuildOptions.define,
|
|
231
|
+
'__MANTAJS_OVERRIDES__': JSON.stringify(
|
|
232
|
+
[...overrides.keys()].sort().join(',')
|
|
233
|
+
),
|
|
234
|
+
}
|
|
235
|
+
config.optimizeDeps.force = true
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
configureServer(server: ViteDevServer) {
|
|
239
|
+
// Always update server ref (called again after each server.restart())
|
|
240
|
+
currentServer = server
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(componentsDir)) return
|
|
243
|
+
|
|
244
|
+
// Create ONE independent watcher that survives server.restart().
|
|
245
|
+
// Uses Node's fs.watch (FSEvents on macOS) — lightweight, no polling.
|
|
246
|
+
if (!watcherCreated) {
|
|
247
|
+
watcherCreated = true
|
|
248
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
249
|
+
|
|
250
|
+
fs.watch(componentsDir, { recursive: true }, (_event, filename) => {
|
|
251
|
+
if (!filename) return
|
|
252
|
+
const ext = path.extname(filename)
|
|
253
|
+
if (!COMPONENT_EXT_SET.has(ext)) return
|
|
254
|
+
|
|
255
|
+
const name = path.basename(filename).replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
256
|
+
if (!name || name === "index") return
|
|
257
|
+
|
|
258
|
+
// Only act if this file name matches a dashboard component
|
|
259
|
+
if (!dashboardComponents.has(name)) return
|
|
260
|
+
|
|
261
|
+
const fullPath = path.resolve(componentsDir, filename)
|
|
262
|
+
const fileExists = fs.existsSync(fullPath)
|
|
263
|
+
const wasKnown = knownOverrideFiles.has(fullPath)
|
|
264
|
+
|
|
265
|
+
if (fileExists && wasKnown) {
|
|
266
|
+
// MODIFICATION — send HMR update ourselves. After server.restart(),
|
|
267
|
+
// Vite's internal chokidar may not fire for override files, so we
|
|
268
|
+
// handle it entirely from our independent fs.watch.
|
|
269
|
+
const mods = currentServer?.moduleGraph.getModulesByFile(fullPath)
|
|
270
|
+
if (mods && mods.size > 0) {
|
|
271
|
+
for (const mod of mods) {
|
|
272
|
+
currentServer!.moduleGraph.invalidateModule(mod)
|
|
154
273
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
contents,
|
|
186
|
-
loader: loader as any,
|
|
187
|
-
resolveDir: path.dirname(overridePath),
|
|
188
|
-
}
|
|
274
|
+
currentServer!.ws.send({
|
|
275
|
+
type: "update",
|
|
276
|
+
updates: [...mods].map((mod) => ({
|
|
277
|
+
type: "js-update" as const,
|
|
278
|
+
path: mod.url,
|
|
279
|
+
acceptedPath: mod.url,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
explicitImportRequired: false,
|
|
282
|
+
})),
|
|
283
|
+
})
|
|
284
|
+
console.log(`[custom-dashboard] Override "${name}" modified → HMR`)
|
|
285
|
+
} else {
|
|
286
|
+
console.log(`[custom-dashboard] Override "${name}" not in graph → force-reload`)
|
|
287
|
+
currentServer?.ws.send({ type: "custom", event: "mantajs:force-reload" })
|
|
288
|
+
}
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// CREATION or DELETION — the esbuild pre-bundle must be rebuilt
|
|
293
|
+
// because the chunk structure changes (new external ref or removed).
|
|
294
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
295
|
+
debounceTimer = setTimeout(async () => {
|
|
296
|
+
// Re-scan overrides from disk
|
|
297
|
+
overridesByName.clear()
|
|
298
|
+
knownOverrideFiles.clear()
|
|
299
|
+
for (const fp of collectComponentFiles(componentsDir).sort()) {
|
|
300
|
+
const n = path.basename(fp).replace(/\.(tsx?|jsx?|mts|mjs)$/, "")
|
|
301
|
+
if (n && n !== "index") {
|
|
302
|
+
overridesByName.set(n, fp)
|
|
303
|
+
knownOverrideFiles.add(fp)
|
|
189
304
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const action = fileExists ? "created" : "deleted"
|
|
308
|
+
console.log(`[custom-dashboard] Override "${name}" ${action} → restarting...`)
|
|
309
|
+
console.log(`[custom-dashboard] overrides:`, [...overridesByName.keys()])
|
|
310
|
+
|
|
311
|
+
// Vite preserves the WebSocket connection across restart() — the
|
|
312
|
+
// browser never disconnects. Await restart, then tell the client
|
|
313
|
+
// to do a cache-busting reload (location.reload() reuses cached
|
|
314
|
+
// modules; our custom event navigates to a timestamped URL instead).
|
|
315
|
+
try {
|
|
316
|
+
if (!currentServer) {
|
|
317
|
+
console.warn(`[custom-dashboard] No server available for restart`)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
await currentServer.restart()
|
|
321
|
+
currentServer.ws.send({
|
|
322
|
+
type: "custom",
|
|
323
|
+
event: "mantajs:force-reload",
|
|
324
|
+
})
|
|
325
|
+
console.log(`[custom-dashboard] Force-reload sent to browser`)
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error(`[custom-dashboard] Restart failed:`, e)
|
|
328
|
+
}
|
|
329
|
+
}, 300)
|
|
193
330
|
})
|
|
331
|
+
}
|
|
332
|
+
},
|
|
194
333
|
|
|
195
|
-
|
|
196
|
-
|
|
334
|
+
handleHotUpdate({ file }) {
|
|
335
|
+
// Suppress Vite's default HMR for override files — our fs.watch
|
|
336
|
+
// handles modifications (HMR) and deletions (restart) instead.
|
|
337
|
+
if (knownOverrideFiles.has(file)) {
|
|
338
|
+
return []
|
|
197
339
|
}
|
|
198
340
|
},
|
|
199
341
|
|
|
342
|
+
transformIndexHtml(html) {
|
|
343
|
+
// Inject a client-side script that listens for our force-reload event.
|
|
344
|
+
// Unlike location.reload(), this navigates to a cache-busting URL so
|
|
345
|
+
// the browser re-fetches all modules (including pre-bundled chunks).
|
|
346
|
+
return html.replace(
|
|
347
|
+
"</head>",
|
|
348
|
+
`<script type="module">
|
|
349
|
+
if (import.meta.hot) {
|
|
350
|
+
import.meta.hot.on("mantajs:force-reload", () => {
|
|
351
|
+
const url = new URL(location.href);
|
|
352
|
+
url.searchParams.set("_r", Date.now().toString());
|
|
353
|
+
location.replace(url.href);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
</script>
|
|
357
|
+
</head>`
|
|
358
|
+
)
|
|
359
|
+
},
|
|
360
|
+
|
|
200
361
|
resolveId(source) {
|
|
201
362
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID
|
|
363
|
+
// Resolve override imports to the actual file path — Vite then serves
|
|
364
|
+
// the file through its transform pipeline (including React Fast Refresh).
|
|
365
|
+
if (source.startsWith(OVERRIDE_PREFIX)) {
|
|
366
|
+
return source.slice(OVERRIDE_PREFIX.length)
|
|
367
|
+
}
|
|
202
368
|
return null
|
|
203
369
|
},
|
|
204
370
|
|