Ctrl K
ComunidadGuías…

vite.config.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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,
  },
});