vite.config.ts
Rama main · réplica de lectura del repositorio original.
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
// Intercept console.error to suppress proxy errors in development
// These errors are normal when the backend is restarting or connections are closed
if (process.env.NODE_ENV !== "production") {
const originalConsoleError = console.error;
console.error = (...args: any[]) => {
const message = args.join(" ");
// Suppress Vite WebSocket proxy errors - they are normal during development
if (
message.includes("[vite] ws proxy error") ||
message.includes("This socket has been ended by the other party") ||
(message.includes("[vite]") &&
message.includes("proxy error") &&
message.includes("socket")) ||
// Suppress HTTP proxy errors during backend restart
(message.includes("[vite] http proxy error") &&
message.includes("ECONNREFUSED")) ||
// Suppress WebSocket connection errors
(message.includes("WebSocket connection to") &&
message.includes("failed")) ||
message.includes("Invalid frame header")
) {
// Silently ignore - these are expected during development
return;
}
// Log all other errors normally
originalConsoleError.apply(console, args);
};
}
export default defineConfig({
base: "/",
// Asset URLs inside JS and CSS (lazy-chunk preloads, fonts, images) are resolved
// relative to the importing file instead of "/", so they keep the Home Assistant
// Ingress prefix (/api/hassio_ingress/<token>/). index.html keeps absolute paths:
// the server prefixes them under Ingress, and they stay valid on nested SPA routes.
experimental: {
renderBuiltUrl: (_filename, { hostType }) =>
hostType === "html" ? undefined : { relative: true },
},
server: {
port: parseInt(process.env.VITE_PORT || "5173", 10),
host: "0.0.0.0", // Listen on all interfaces to allow access via IP
allowedHosts: ["mwk-dev.myoueb.fr", "192.168.32.150", "192.168.32.202", "localhost"],
// Disable cache in dev so CSS/JS changes are visible immediately
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
},
// Configure HMR WebSocket
// In Docker dev, use DASHBOARD_PORT (host port) instead of VITE_PORT (container port)
// Vite will automatically detect the host from the browser's window.location.hostname
// clientPort should be the port the browser connects to (host port in Docker)
hmr: {
clientPort: parseInt(
process.env.DASHBOARD_PORT || process.env.VITE_PORT || "5173",
10,
),
// Vite will automatically detect the host from the browser's window.location.hostname
// So if you access via 192.168.1.150:3666, HMR will use ws://192.168.1.150:3666
},
proxy: {
"/api": {
// Use localhost for proxy - Vite proxy runs on the same machine as the backend
// When accessing via IP (192.168.1.150), the proxy still connects to localhost:3003
// because the proxy runs server-side on the same machine
// IMPORTANT: Use PORT (container port) not SERVER_PORT (host port) in Docker
// In Docker dev: PORT=3003 (container), SERVER_PORT=3668 (host)
// In npm dev: PORT=3003 or SERVER_PORT=3003 (same value)
target: `http://127.0.0.1:${process.env.PORT || process.env.SERVER_PORT || "3003"}`,
changeOrigin: true,
secure: false,
timeout: 60000, // Increased timeout for long-running requests like ping
proxyTimeout: 60000,
configure: (proxy, _options) => {
proxy.on("error", (err, req, res) => {
// Suppress ECONNREFUSED errors during backend restart - they are normal
const errorMessage = err?.message || String(err || "");
if (errorMessage.includes("ECONNREFUSED")) {
// Return a proper JSON error response so the frontend can handle it gracefully
if (res && !res.headersSent) {
res.writeHead(503, {
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
success: false,
error: {
code: "CONNECTION_REFUSED",
message:
"Le serveur n'est pas disponible. Reconnexion en cours...",
temporary: true,
},
}),
);
}
return;
}
// Handle other proxy errors gracefully
if (res && !res.headersSent) {
res.writeHead(500, {
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
success: false,
error: { code: "PROXY_ERROR", message: "Erreur de proxy" },
}),
);
}
});
},
},
"/ws": {
// IMPORTANT: Use PORT (container port) not SERVER_PORT (host port) in Docker
// In Docker dev: PORT=3003 (container), SERVER_PORT=3668 (host)
// In npm dev: PORT=3003 or SERVER_PORT=3003 (same value)
target: `ws://127.0.0.1:${process.env.PORT || process.env.SERVER_PORT || "3003"}`,
ws: true,
changeOrigin: true,
secure: false,
rewrite: (path) => path, // Don't rewrite the path, pass it as-is
configure: (proxy, _options) => {
// Suppress all WebSocket proxy errors - they are normal during connection attempts
// The frontend will automatically retry connecting
const shouldSuppressError = (err: any): boolean => {
const errorMessage = err?.message || String(err || "");
return (
errorMessage.includes("socket") ||
errorMessage.includes("ECONNRESET") ||
errorMessage.includes("ECONNREFUSED") ||
errorMessage.includes("ended by the other party") ||
errorMessage.includes("ECONNABORTED") ||
errorMessage.includes("ETIMEDOUT")
);
};
// Handle WebSocket proxy errors - suppress all common errors
proxy.on("error", (err, _req, _res) => {
if (!shouldSuppressError(err)) {
console.error(
"[Vite WS Proxy] Unexpected error:",
err?.message || String(err),
);
}
// Silently ignore all other errors
});
// Handle WebSocket upgrade errors
proxy.on("proxyReqWs", (proxyReq, req, socket) => {
socket.on("error", (err) => {
// Silently suppress all socket errors during upgrade
if (!shouldSuppressError(err)) {
console.error(
"[Vite WS Proxy] Socket error:",
err?.message || String(err),
);
}
});
// Handle socket close during upgrade - normal behavior
socket.on("close", () => {
// Silently handle - this is normal
});
});
// Handle WebSocket connection close - normal behavior
proxy.on("close", (_res, _socket, _head) => {
// Silently handle connection closes - this is normal
});
// Handle WebSocket upgrade response errors
(proxy as any).on(
"proxyResWs",
(_proxyRes: any, _req: any, socket: any) => {
socket.on("error", (err) => {
// Silently suppress all errors after upgrade
if (!shouldSuppressError(err)) {
console.error(
"[Vite WS Proxy] Post-upgrade error:",
err?.message || String(err),
);
}
});
},
);
},
},
},
},
// Expose backend port to the frontend so the WebSocket can connect directly
// in dev mode, bypassing Vite's WS server which conflicts with HMR on the same path.
// - npm dev: PORT=3003, SERVER_PORT=3003 → browser connects to :3003
// - Docker dev: PORT=3003, SERVER_PORT=3668 → browser connects to :3668 (host-mapped port)
define: {
"import.meta.env.VITE_BACKEND_PORT": JSON.stringify(
process.env.SERVER_PORT || process.env.PORT || "3003",
),
},
plugins: [
react(),
// Progressive Web App — adds a manifest + service worker so Chrome/Edge
// shows an "Install app" button in the URL bar. Online-first by design
// (the app monitors a live network — stale cache would mislead the user)
// but cached app-shell speeds up the next visit.
VitePWA({
registerType: "autoUpdate",
includeAssets: ["svg/favicon-racine.svg"],
manifest: {
name: "MyNetwork",
short_name: "MyNetwork",
description:
"Multi-Source Network Dashboard (Freebox, UniFi, network scans)",
theme_color: "#0f172a",
background_color: "#050505",
display: "standalone",
scope: "/",
start_url: "/",
icons: [
{ src: "/pwa-icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/pwa-icon-512.png", sizes: "512x512", type: "image/png" },
{
src: "/pwa-icon-maskable-512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
// Activate the new SW as soon as it's available so users on a tab
// opened before the deploy don't keep hitting stale chunk hashes
// (which would 404 → SPA fallback → MIME mismatch → ErrorBoundary).
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true,
// Disable the default navigateFallback: it registers a NavigationRoute
// that serves the precached index.html CacheFirst (network is never
// even tried), so any client whose SW hasn't finished updating yet
// gets served an old shell referencing chunk hashes that a later
// deploy has already deleted from disk — even on a brand new
// navigation (e.g. reopening the browser). The Express server already
// serves index.html with no-cache and handles the SPA fallback, so
// navigation doesn't need the SW's app-shell precache to work online.
navigateFallback: null,
// No runtimeCaching for scripts: hash-named chunks must always hit the
// network so a fresh deploy is never shadowed by a cached old chunk.
runtimeCaching: [
{
// Prefer the live index.html (always fresh); only fall back to the
// cached shell when the network is genuinely unavailable.
urlPattern: ({ request }) => request.mode === "navigate",
handler: "NetworkFirst",
options: { cacheName: "mynetwork-shell", networkTimeoutSeconds: 3 },
},
{
urlPattern: ({ request }) =>
request.destination === "image" || request.destination === "font",
handler: "StaleWhileRevalidate",
options: { cacheName: "mynetwork-assets" },
},
],
},
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
build: {
sourcemap: false,
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes("node_modules")) {
// Topology / flow / dagre / image-export stack. Comes BEFORE
// the recharts catch so @xyflow's d3 deps (d3-zoom / d3-drag /
// d3-selection / d3-interpolate) don't get pulled in with
// recharts and crash both bundles at init ("_ is not a
// function" on prod for v0.7.93).
if (
id.includes("@xyflow/") ||
id.includes("/dagre/") ||
id.includes("html-to-image") ||
id.includes("jspdf")
) {
return "vendor-topology";
}
// Recharts only — its d3 deps fall through to the default
// 'vendor' chunk so they're shared cleanly with @xyflow.
if (id.includes("recharts")) {
return "vendor-charts";
}
// Lucide icons
if (id.includes("lucide-react")) {
return "vendor-icons";
}
// Zustand state management
if (id.includes("zustand")) {
return "vendor-state";
}
// Markdown rendering
if (
id.includes("react-markdown") ||
id.includes("remark") ||
id.includes("rehype") ||
id.includes("unified") ||
id.includes("mdast") ||
id.includes("hast") ||
id.includes("micromark") ||
id.includes("vfile")
) {
return "vendor-markdown";
}
// i18n
if (id.includes("i18next") || id.includes("react-i18next")) {
return "vendor-i18n";
}
// Keep React/React-DOM in main chunk
if (
id.includes("/react/") ||
id.includes("/react-dom/") ||
id.includes("/react-is/")
) {
return undefined;
}
return "vendor";
}
},
},
},
// Increase chunk size warning limit to 600 KB (optional, but we're splitting anyway)
chunkSizeWarningLimit: 600,
},
});