@mantajs/dashboard 0.1.16 → 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 +116 -98
- package/dist/vite-plugin/index.mjs +116 -98
- package/package.json +1 -1
- package/src/vite-plugin/index.ts +181 -136
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 = [];
|
|
@@ -101,38 +94,33 @@ function findDashboardSrc() {
|
|
|
101
94
|
function customDashboardPlugin() {
|
|
102
95
|
const componentsDir = import_path.default.resolve(process.cwd(), "src/admin/components");
|
|
103
96
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
104
|
-
const
|
|
105
|
-
const knownDashboardComponents = /* @__PURE__ */ new Set();
|
|
97
|
+
const dashboardComponents = /* @__PURE__ */ new Set();
|
|
106
98
|
const dashboardSrc = findDashboardSrc();
|
|
107
99
|
if (dashboardSrc) {
|
|
108
100
|
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
109
101
|
const cName = getComponentName(f);
|
|
110
|
-
if (cName)
|
|
102
|
+
if (cName) dashboardComponents.add(cName);
|
|
111
103
|
}
|
|
112
104
|
}
|
|
113
105
|
if (import_fs.default.existsSync(componentsDir)) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const fileName = import_path.default.basename(fullPath);
|
|
117
|
-
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)$/, "");
|
|
118
108
|
if (name && name !== "index") {
|
|
119
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
120
|
-
console.warn(
|
|
121
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
109
|
overridesByName.set(name, fullPath);
|
|
125
110
|
}
|
|
126
111
|
}
|
|
127
112
|
}
|
|
128
113
|
const hasOverrides = overridesByName.size > 0;
|
|
114
|
+
let currentServer = null;
|
|
115
|
+
let watcherCreated = false;
|
|
116
|
+
const knownOverrideFiles = new Set(overridesByName.values());
|
|
129
117
|
if (process.env.NODE_ENV === "development") {
|
|
130
118
|
if (hasOverrides) {
|
|
131
119
|
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
132
120
|
}
|
|
133
|
-
if (
|
|
121
|
+
if (dashboardComponents.size > 0) {
|
|
134
122
|
console.log(
|
|
135
|
-
`[custom-dashboard] Scanned ${
|
|
123
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
136
124
|
);
|
|
137
125
|
}
|
|
138
126
|
}
|
|
@@ -149,6 +137,10 @@ function customDashboardPlugin() {
|
|
|
149
137
|
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
150
138
|
name: "dashboard-component-overrides",
|
|
151
139
|
setup(build) {
|
|
140
|
+
build.onResolve({ filter: /^__mantajs_override__:/ }, (args) => ({
|
|
141
|
+
path: args.path,
|
|
142
|
+
external: true
|
|
143
|
+
}));
|
|
152
144
|
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
153
145
|
if (overrides.size === 0) return void 0;
|
|
154
146
|
const normalized = args.path.replace(/\\/g, "/");
|
|
@@ -161,15 +153,9 @@ function customDashboardPlugin() {
|
|
|
161
153
|
return void 0;
|
|
162
154
|
}
|
|
163
155
|
if (process.env.NODE_ENV === "development") {
|
|
164
|
-
console.log(
|
|
165
|
-
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
166
|
-
);
|
|
156
|
+
console.log(`[custom-dashboard] Redirecting entry \u2192 ${srcEntry}`);
|
|
167
157
|
}
|
|
168
|
-
return {
|
|
169
|
-
contents,
|
|
170
|
-
loader: "tsx",
|
|
171
|
-
resolveDir: import_path.default.dirname(srcEntry)
|
|
172
|
-
};
|
|
158
|
+
return { contents, loader: "tsx", resolveDir: import_path.default.dirname(srcEntry) };
|
|
173
159
|
});
|
|
174
160
|
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
175
161
|
if (overrides.size === 0) return void 0;
|
|
@@ -180,92 +166,124 @@ function customDashboardPlugin() {
|
|
|
180
166
|
const componentName = getComponentName(args.path);
|
|
181
167
|
if (componentName && overrides.has(componentName)) {
|
|
182
168
|
const overridePath = overrides.get(componentName);
|
|
183
|
-
const
|
|
184
|
-
const loader = VALID_LOADERS[ext] || "tsx";
|
|
185
|
-
let contents;
|
|
186
|
-
try {
|
|
187
|
-
contents = import_fs.default.readFileSync(overridePath, "utf-8");
|
|
188
|
-
} catch {
|
|
189
|
-
return void 0;
|
|
190
|
-
}
|
|
191
|
-
appliedOverrides.add(componentName);
|
|
169
|
+
const normalizedPath = overridePath.replace(/\\/g, "/");
|
|
192
170
|
if (process.env.NODE_ENV === "development") {
|
|
193
|
-
console.log(
|
|
194
|
-
`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
|
|
195
|
-
);
|
|
171
|
+
console.log(`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`);
|
|
196
172
|
}
|
|
197
173
|
return {
|
|
198
|
-
contents
|
|
199
|
-
loader,
|
|
200
|
-
resolveDir: import_path.default.dirname(
|
|
174
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
175
|
+
loader: "tsx",
|
|
176
|
+
resolveDir: import_path.default.dirname(args.path)
|
|
201
177
|
};
|
|
202
178
|
}
|
|
203
179
|
return void 0;
|
|
204
180
|
});
|
|
205
181
|
}
|
|
206
182
|
});
|
|
183
|
+
config.optimizeDeps.esbuildOptions.define = {
|
|
184
|
+
...config.optimizeDeps.esbuildOptions.define,
|
|
185
|
+
"__MANTAJS_OVERRIDES__": JSON.stringify(
|
|
186
|
+
[...overrides.keys()].sort().join(",")
|
|
187
|
+
)
|
|
188
|
+
};
|
|
207
189
|
config.optimizeDeps.force = true;
|
|
208
190
|
},
|
|
209
191
|
configureServer(server) {
|
|
192
|
+
currentServer = server;
|
|
210
193
|
if (!import_fs.default.existsSync(componentsDir)) return;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
212
|
+
}
|
|
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" });
|
|
232
227
|
}
|
|
228
|
+
return;
|
|
233
229
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
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
|
+
);
|
|
266
281
|
},
|
|
267
282
|
resolveId(source) {
|
|
268
283
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
|
284
|
+
if (source.startsWith(OVERRIDE_PREFIX)) {
|
|
285
|
+
return source.slice(OVERRIDE_PREFIX.length);
|
|
286
|
+
}
|
|
269
287
|
return null;
|
|
270
288
|
},
|
|
271
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 = [];
|
|
@@ -66,38 +59,33 @@ function findDashboardSrc() {
|
|
|
66
59
|
function customDashboardPlugin() {
|
|
67
60
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components");
|
|
68
61
|
const overridesByName = /* @__PURE__ */ new Map();
|
|
69
|
-
const
|
|
70
|
-
const knownDashboardComponents = /* @__PURE__ */ new Set();
|
|
62
|
+
const dashboardComponents = /* @__PURE__ */ new Set();
|
|
71
63
|
const dashboardSrc = findDashboardSrc();
|
|
72
64
|
if (dashboardSrc) {
|
|
73
65
|
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
74
66
|
const cName = getComponentName(f);
|
|
75
|
-
if (cName)
|
|
67
|
+
if (cName) dashboardComponents.add(cName);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
70
|
if (fs.existsSync(componentsDir)) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const fileName = path.basename(fullPath);
|
|
82
|
-
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)$/, "");
|
|
83
73
|
if (name && name !== "index") {
|
|
84
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
85
|
-
console.warn(
|
|
86
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
74
|
overridesByName.set(name, fullPath);
|
|
90
75
|
}
|
|
91
76
|
}
|
|
92
77
|
}
|
|
93
78
|
const hasOverrides = overridesByName.size > 0;
|
|
79
|
+
let currentServer = null;
|
|
80
|
+
let watcherCreated = false;
|
|
81
|
+
const knownOverrideFiles = new Set(overridesByName.values());
|
|
94
82
|
if (process.env.NODE_ENV === "development") {
|
|
95
83
|
if (hasOverrides) {
|
|
96
84
|
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()]);
|
|
97
85
|
}
|
|
98
|
-
if (
|
|
86
|
+
if (dashboardComponents.size > 0) {
|
|
99
87
|
console.log(
|
|
100
|
-
`[custom-dashboard] Scanned ${
|
|
88
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
101
89
|
);
|
|
102
90
|
}
|
|
103
91
|
}
|
|
@@ -114,6 +102,10 @@ function customDashboardPlugin() {
|
|
|
114
102
|
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
115
103
|
name: "dashboard-component-overrides",
|
|
116
104
|
setup(build) {
|
|
105
|
+
build.onResolve({ filter: /^__mantajs_override__:/ }, (args) => ({
|
|
106
|
+
path: args.path,
|
|
107
|
+
external: true
|
|
108
|
+
}));
|
|
117
109
|
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
118
110
|
if (overrides.size === 0) return void 0;
|
|
119
111
|
const normalized = args.path.replace(/\\/g, "/");
|
|
@@ -126,15 +118,9 @@ function customDashboardPlugin() {
|
|
|
126
118
|
return void 0;
|
|
127
119
|
}
|
|
128
120
|
if (process.env.NODE_ENV === "development") {
|
|
129
|
-
console.log(
|
|
130
|
-
`[custom-dashboard] Redirecting entry: ${args.path} \u2192 ${srcEntry}`
|
|
131
|
-
);
|
|
121
|
+
console.log(`[custom-dashboard] Redirecting entry \u2192 ${srcEntry}`);
|
|
132
122
|
}
|
|
133
|
-
return {
|
|
134
|
-
contents,
|
|
135
|
-
loader: "tsx",
|
|
136
|
-
resolveDir: path.dirname(srcEntry)
|
|
137
|
-
};
|
|
123
|
+
return { contents, loader: "tsx", resolveDir: path.dirname(srcEntry) };
|
|
138
124
|
});
|
|
139
125
|
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
140
126
|
if (overrides.size === 0) return void 0;
|
|
@@ -145,92 +131,124 @@ function customDashboardPlugin() {
|
|
|
145
131
|
const componentName = getComponentName(args.path);
|
|
146
132
|
if (componentName && overrides.has(componentName)) {
|
|
147
133
|
const overridePath = overrides.get(componentName);
|
|
148
|
-
const
|
|
149
|
-
const loader = VALID_LOADERS[ext] || "tsx";
|
|
150
|
-
let contents;
|
|
151
|
-
try {
|
|
152
|
-
contents = fs.readFileSync(overridePath, "utf-8");
|
|
153
|
-
} catch {
|
|
154
|
-
return void 0;
|
|
155
|
-
}
|
|
156
|
-
appliedOverrides.add(componentName);
|
|
134
|
+
const normalizedPath = overridePath.replace(/\\/g, "/");
|
|
157
135
|
if (process.env.NODE_ENV === "development") {
|
|
158
|
-
console.log(
|
|
159
|
-
`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`
|
|
160
|
-
);
|
|
136
|
+
console.log(`[custom-dashboard] Override: ${componentName} \u2192 ${overridePath}`);
|
|
161
137
|
}
|
|
162
138
|
return {
|
|
163
|
-
contents
|
|
164
|
-
loader,
|
|
165
|
-
resolveDir: path.dirname(
|
|
139
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
140
|
+
loader: "tsx",
|
|
141
|
+
resolveDir: path.dirname(args.path)
|
|
166
142
|
};
|
|
167
143
|
}
|
|
168
144
|
return void 0;
|
|
169
145
|
});
|
|
170
146
|
}
|
|
171
147
|
});
|
|
148
|
+
config.optimizeDeps.esbuildOptions.define = {
|
|
149
|
+
...config.optimizeDeps.esbuildOptions.define,
|
|
150
|
+
"__MANTAJS_OVERRIDES__": JSON.stringify(
|
|
151
|
+
[...overrides.keys()].sort().join(",")
|
|
152
|
+
)
|
|
153
|
+
};
|
|
172
154
|
config.optimizeDeps.force = true;
|
|
173
155
|
},
|
|
174
156
|
configureServer(server) {
|
|
157
|
+
currentServer = server;
|
|
175
158
|
if (!fs.existsSync(componentsDir)) return;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
177
|
+
}
|
|
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" });
|
|
197
192
|
}
|
|
193
|
+
return;
|
|
198
194
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
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
|
+
);
|
|
231
246
|
},
|
|
232
247
|
resolveId(source) {
|
|
233
248
|
if (source === MENU_VIRTUAL_ID) return MENU_RESOLVED_ID;
|
|
249
|
+
if (source.startsWith(OVERRIDE_PREFIX)) {
|
|
250
|
+
return source.slice(OVERRIDE_PREFIX.length);
|
|
251
|
+
}
|
|
234
252
|
return null;
|
|
235
253
|
},
|
|
236
254
|
load(id) {
|
package/package.json
CHANGED
package/src/vite-plugin/index.ts
CHANGED
|
@@ -5,18 +5,13 @@ import fs from "fs"
|
|
|
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
|
|
@@ -90,41 +85,44 @@ function findDashboardSrc(): string | null {
|
|
|
90
85
|
*
|
|
91
86
|
* Handles:
|
|
92
87
|
* 1. Menu config virtual module (virtual:dashboard/menu-config)
|
|
93
|
-
* 2. Component overrides — any file in src/admin/components/
|
|
94
|
-
* 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.
|
|
95
105
|
*/
|
|
96
106
|
export function customDashboardPlugin(): Plugin {
|
|
97
107
|
const componentsDir = path.resolve(process.cwd(), "src/admin/components")
|
|
98
108
|
const overridesByName = new Map<string, string>()
|
|
99
109
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
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>()
|
|
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>()
|
|
109
113
|
const dashboardSrc = findDashboardSrc()
|
|
110
114
|
if (dashboardSrc) {
|
|
111
115
|
for (const f of collectComponentFiles(dashboardSrc)) {
|
|
112
116
|
const cName = getComponentName(f)
|
|
113
|
-
if (cName)
|
|
117
|
+
if (cName) dashboardComponents.add(cName)
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// Collect initial overrides from disk
|
|
117
122
|
if (fs.existsSync(componentsDir)) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const fileName = path.basename(fullPath)
|
|
121
|
-
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)$/, "")
|
|
122
125
|
if (name && name !== "index") {
|
|
123
|
-
if (overridesByName.has(name) && process.env.NODE_ENV === "development") {
|
|
124
|
-
console.warn(
|
|
125
|
-
`[custom-dashboard] Duplicate override "${name}": ${overridesByName.get(name)} will be replaced by ${fullPath}`
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
126
|
overridesByName.set(name, fullPath)
|
|
129
127
|
}
|
|
130
128
|
}
|
|
@@ -132,13 +130,20 @@ export function customDashboardPlugin(): Plugin {
|
|
|
132
130
|
|
|
133
131
|
const hasOverrides = overridesByName.size > 0
|
|
134
132
|
|
|
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
|
+
|
|
135
140
|
if (process.env.NODE_ENV === "development") {
|
|
136
141
|
if (hasOverrides) {
|
|
137
142
|
console.log("[custom-dashboard] overrides:", [...overridesByName.keys()])
|
|
138
143
|
}
|
|
139
|
-
if (
|
|
144
|
+
if (dashboardComponents.size > 0) {
|
|
140
145
|
console.log(
|
|
141
|
-
`[custom-dashboard] Scanned ${
|
|
146
|
+
`[custom-dashboard] Scanned ${dashboardComponents.size} dashboard components`
|
|
142
147
|
)
|
|
143
148
|
}
|
|
144
149
|
}
|
|
@@ -148,14 +153,10 @@ export function customDashboardPlugin(): Plugin {
|
|
|
148
153
|
enforce: "pre",
|
|
149
154
|
|
|
150
155
|
config(config) {
|
|
151
|
-
// Always exclude the menu virtual module
|
|
152
156
|
config.optimizeDeps = config.optimizeDeps || {}
|
|
153
157
|
config.optimizeDeps.exclude = config.optimizeDeps.exclude || []
|
|
154
158
|
config.optimizeDeps.exclude.push(MENU_VIRTUAL_ID)
|
|
155
159
|
|
|
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
160
|
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {}
|
|
160
161
|
config.optimizeDeps.esbuildOptions.plugins =
|
|
161
162
|
config.optimizeDeps.esbuildOptions.plugins || []
|
|
@@ -164,18 +165,21 @@ export function customDashboardPlugin(): Plugin {
|
|
|
164
165
|
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
165
166
|
name: "dashboard-component-overrides",
|
|
166
167
|
setup(build) {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
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
|
|
169
177
|
build.onLoad({ filter: /app\.(mjs|js)$/ }, (args) => {
|
|
170
|
-
// Only activate when there are overrides
|
|
171
178
|
if (overrides.size === 0) return undefined
|
|
172
|
-
|
|
173
179
|
const normalized = args.path.replace(/\\/g, "/")
|
|
174
180
|
if (!normalized.includes("/dashboard/dist/")) return undefined
|
|
175
181
|
|
|
176
|
-
const srcEntry = normalized
|
|
177
|
-
.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx")
|
|
178
|
-
|
|
182
|
+
const srcEntry = normalized.replace(/\/dist\/app\.(mjs|js)$/, "/src/app.tsx")
|
|
179
183
|
let contents: string
|
|
180
184
|
try {
|
|
181
185
|
contents = fs.readFileSync(srcEntry, "utf-8")
|
|
@@ -184,53 +188,34 @@ export function customDashboardPlugin(): Plugin {
|
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
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),
|
|
191
|
+
console.log(`[custom-dashboard] Redirecting entry → ${srcEntry}`)
|
|
195
192
|
}
|
|
193
|
+
return { contents, loader: "tsx", resolveDir: path.dirname(srcEntry) }
|
|
196
194
|
})
|
|
197
195
|
|
|
198
|
-
//
|
|
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
199
|
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, (args) => {
|
|
200
200
|
if (overrides.size === 0) return undefined
|
|
201
|
-
|
|
202
201
|
const normalized = args.path.replace(/\\/g, "/")
|
|
203
202
|
if (!normalized.includes("/dashboard/src/")) return undefined
|
|
204
203
|
|
|
205
|
-
// Skip index/barrel files to preserve re-exports
|
|
206
204
|
const fileName = path.basename(args.path)
|
|
207
205
|
if (fileName.startsWith("index.")) return undefined
|
|
208
206
|
|
|
209
207
|
const componentName = getComponentName(args.path)
|
|
210
208
|
if (componentName && overrides.has(componentName)) {
|
|
211
209
|
const overridePath = overrides.get(componentName)!
|
|
212
|
-
const
|
|
213
|
-
const loader = VALID_LOADERS[ext] || "tsx"
|
|
214
|
-
|
|
215
|
-
let contents: string
|
|
216
|
-
try {
|
|
217
|
-
contents = fs.readFileSync(overridePath, "utf-8")
|
|
218
|
-
} catch {
|
|
219
|
-
return undefined
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Track this as a real applied override
|
|
223
|
-
appliedOverrides.add(componentName)
|
|
210
|
+
const normalizedPath = overridePath.replace(/\\/g, "/")
|
|
224
211
|
|
|
225
212
|
if (process.env.NODE_ENV === "development") {
|
|
226
|
-
console.log(
|
|
227
|
-
`[custom-dashboard] Override: ${componentName} → ${overridePath}`
|
|
228
|
-
)
|
|
213
|
+
console.log(`[custom-dashboard] Override: ${componentName} → ${overridePath}`)
|
|
229
214
|
}
|
|
230
215
|
return {
|
|
231
|
-
contents
|
|
232
|
-
loader:
|
|
233
|
-
resolveDir: path.dirname(
|
|
216
|
+
contents: `export * from "${OVERRIDE_PREFIX}${normalizedPath}"`,
|
|
217
|
+
loader: "tsx",
|
|
218
|
+
resolveDir: path.dirname(args.path),
|
|
234
219
|
}
|
|
235
220
|
}
|
|
236
221
|
return undefined
|
|
@@ -238,88 +223,148 @@ export function customDashboardPlugin(): Plugin {
|
|
|
238
223
|
},
|
|
239
224
|
})
|
|
240
225
|
|
|
241
|
-
//
|
|
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
|
+
}
|
|
242
235
|
config.optimizeDeps.force = true
|
|
243
236
|
},
|
|
244
237
|
|
|
245
238
|
configureServer(server: ViteDevServer) {
|
|
246
|
-
|
|
239
|
+
// Always update server ref (called again after each server.restart())
|
|
240
|
+
currentServer = server
|
|
247
241
|
|
|
248
|
-
|
|
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
|
|
260
|
-
}
|
|
242
|
+
if (!fs.existsSync(componentsDir)) return
|
|
261
243
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
273
|
+
}
|
|
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" })
|
|
273
288
|
}
|
|
289
|
+
return
|
|
274
290
|
}
|
|
275
291
|
|
|
276
|
-
|
|
277
|
-
|
|
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)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
278
306
|
|
|
279
|
-
|
|
280
|
-
`[custom-dashboard] Override "${name}" ${
|
|
281
|
-
|
|
282
|
-
console.log(
|
|
283
|
-
`[custom-dashboard] overrides:`,
|
|
284
|
-
[...overridesByName.keys()]
|
|
285
|
-
)
|
|
307
|
+
const action = fileExists ? "created" : "deleted"
|
|
308
|
+
console.log(`[custom-dashboard] Override "${name}" ${action} → restarting...`)
|
|
309
|
+
console.log(`[custom-dashboard] overrides:`, [...overridesByName.keys()])
|
|
286
310
|
|
|
287
|
-
|
|
288
|
-
|
|
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)
|
|
330
|
+
})
|
|
289
331
|
}
|
|
332
|
+
},
|
|
290
333
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
})
|
|
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 []
|
|
339
|
+
}
|
|
340
|
+
},
|
|
310
341
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
)
|
|
319
359
|
},
|
|
320
360
|
|
|
321
361
|
resolveId(source) {
|
|
322
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
|
+
}
|
|
323
368
|
return null
|
|
324
369
|
},
|
|
325
370
|
|