141 lines · 5.7 KB · TypeScript
RawDownload
import { fileURLToPath } from "node:url";import { defineConfig, loadEnv, type Plugin } from "vite";import { cloudflare } from "@cloudflare/vite-plugin";import { tanstackStart } from "@tanstack/react-start/plugin/vite";import react from "@vitejs/plugin-react";import tailwindcss from "@tailwindcss/vite";import executorVitePlugin from "@executor-js/vite-plugin";import { unstable_readConfig } from "wrangler"; import { routes } from "./tsr.routes"; // Dev-only: the cloudflare vite-plugin bridges outbound fetches (JWKS,// OAuth metadata proxy, etc.) through node undici in the host process. If// a pooled keep-alive socket gets RST'd while no listener is attached, the// `'error'` emit is unhandled and tears down the whole dev server. Log// enough to identify the offender and keep the server alive.const devCrashGuard = (): Plugin => {  let installed = false;  const install = () => {    if (installed) return;    installed = true;    process.on("uncaughtException", (err, origin) => {      console.error(`[dev-crash-guard] uncaughtException (origin=${origin}):`, err);    });    process.on("unhandledRejection", (reason, promise) => {      console.error("[dev-crash-guard] unhandledRejection:", reason, promise);    });  };  return {    name: "dev-crash-guard",    apply: "serve",    configureServer: install,  };}; const loadWranglerPublicVars = () => {  const wranglerConfig = unstable_readConfig(    { config: fileURLToPath(new URL("./wrangler.jsonc", import.meta.url)) },    { hideWarnings: true },  );  return Object.fromEntries(    Object.entries(wranglerConfig.vars ?? {}).filter(([key]) => key.startsWith("VITE_PUBLIC_")),  );}; // VITE_PUBLIC_ANALYTICS_PATH is generated once per build by `scripts/build.mjs`// and inherited via process.env, so the client and SSR/Cloudflare environment// builds bake the same value. The fallback "a" is for `vite dev`, where the// proxy isn't routed anyway.const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a"; export default defineConfig(({ command, mode }) => {  const env = loadEnv(mode, process.cwd(), "");  const publicEnv = {    ...loadWranglerPublicVars(),    VITE_PUBLIC_ANALYTICS_PATH: ANALYTICS_PATH,    ...env,  };  // The wrangler-declared OTLP endpoint is for DEPLOYED workers (the  // /v1/traces forwarding route). Under `vite dev` that path is only the  // proxy below — keep the exporter off unless something actually listens  // (e2e/dev sets MOTEL_URL or the env var itself), or every dev session  // posts spans into a dead proxy once a second.  if (command === "serve" && !process.env.MOTEL_URL && !env.VITE_PUBLIC_OTLP_TRACES_URL) {    delete (publicEnv as Record<string, string | undefined>).VITE_PUBLIC_OTLP_TRACES_URL;  }   // Deps vite only discovers once a lazy-loaded React chunk actually renders  // (e.g. opening the MCP/OpenAPI "add source" flow). Discovering them mid-run  // forces a re-optimize + full program reload; in workerd (apps/cloud's SSR  // worker) each reload stacks a new isolate's heap on top of the last one  // without freeing it, so a handful of reloads exhausts the worker's heap  // limit and crashes the dev server. Pre-bundling them at boot means vite  // never discovers them mid-run, so it never reloads. Keep this list scoped  // to deps actually imported by UI code (grep `from "effect/` under  // packages/react/src and packages/plugins/*/src/react, plus js-yaml pulled  // in via packages/plugins/openapi/src/sdk) rather than including the world.  // js-yaml is a transitive dep (via @executor-js/plugin-openapi), not hoisted  // into apps/cloud/node_modules under bun's isolated install, so a plain  // "js-yaml" specifier fails to resolve here and silently falls out of the  // pre-bundle. The "<pkg> > <dep>" syntax resolves it starting from that  // package's own node_modules instead.  const lateDiscoveredDeps = [    "effect/Match",    "effect/Predicate",    "effect/Exit",    "effect/Option",    "effect/Cause",    "effect/Data",    "effect/Schema",    "@executor-js/plugin-openapi > js-yaml",  ];   return {    define: Object.fromEntries(      Object.entries(publicEnv)        .filter(([key]) => key.startsWith("VITE_PUBLIC_"))        .map(([key, value]) => [`import.meta.env.${key}`, JSON.stringify(value)]),    ),    // Browser OTLP spans (VITE_PUBLIC_OTLP_TRACES_URL=/v1/traces, set by the    // e2e global setup) go same-origin and proxy to the local motel server —    // motel serves no CORS headers, so a direct cross-origin post would die    // in preflight. Dev-only; unrouted when nothing listens.    server: {      proxy: {        "/v1/traces": process.env.MOTEL_URL ?? "http://127.0.0.1:27686",      },    },    resolve: { tsconfigPaths: true },    // Client-side pre-bundle. See lateDiscoveredDeps comment above.    optimizeDeps: {      include: lateDiscoveredDeps,    },    // SSR/worker-side pre-bundle for the same deps, keyed under the "ssr"    // environment name that `cloudflare({ viteEnvironment: { name: "ssr" } })`    // below uses. The cloudflare plugin sets its own environments.ssr.optimizeDeps    // (entries/exclude for worker externals); vite merges include arrays rather    // than replacing them, so this only adds to that, it doesn't fight it.    environments: {      ssr: {        optimizeDeps: {          include: lateDiscoveredDeps,        },      },    },    plugins: [      devCrashGuard(),      tailwindcss(),      executorVitePlugin(),      cloudflare({ viteEnvironment: { name: "ssr" }, inspectorPort: false }),      tanstackStart({        // The route tree definition lives in tsr.routes.ts (shared with        // tsr.config.json so `bunx tsr generate` produces the same tree).        router: {          virtualRouteConfig: routes,        },      }),      react(),    ],  };});