280 lines · 11.7 KB · TypeScript
RawDownload
import * as NodeZlib from "node:zlib"; import react, { reactCompilerPreset } from "@vitejs/plugin-react";import babel from "@rolldown/plugin-babel";import { tanstackRouter } from "@tanstack/router-plugin/vite";import compression from "compression";import { defineProject, type TestProjectInlineConfiguration } from "vite-plus/test/config";import "vite-plus/test/config";import { defineConfig, type Connect, type Plugin } from "vite-plus";import pkg from "./package.json" with { type: "json" }; import { DEV_PROXIED_PATH_PREFIXES } from "@t3tools/shared/devProxy"; import { loadRepoEnv } from "../../scripts/lib/public-config";import { tailwindPlugins } from "./vite/tailwind"; const repoEnv = loadRepoEnv();Object.assign(process.env, repoEnv); // Single-origin dev is signalled positively, because it cannot be inferred// from the absence of VITE_HTTP_URL/VITE_WS_URL: the runner deletes those keys// but `loadRepoEnv` merges `.env`/`.env.local` *underneath* the process env, so// a developer with either URL in their `.env` gets it back here. Baking it then// pins the client to localhost and breaks every non-localhost origin — the// exact failure single-origin mode exists to prevent, and an invisible one// since the page still loads.const isSingleOriginDev = process.env.T3CODE_SINGLE_ORIGIN_DEV === "1"; const port = Number(process.env.PORT ?? 5733);const explicitHost = process.env.HOST?.trim();const host = explicitHost || "localhost";const configuredWsUrl = isSingleOriginDev ? undefined : process.env.VITE_WS_URL?.trim();const configuredHttpUrl = isSingleOriginDev ? undefined : process.env.VITE_HTTP_URL?.trim();const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || "";const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || "";const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || "";const configuredClerkCliOAuthClientId = repoEnv.VITE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() || "";const configuredRelayTracingUrl = repoEnv.VITE_RELAY_OTLP_TRACES_URL?.trim() || "";const configuredRelayTracingDataset = repoEnv.VITE_RELAY_OTLP_TRACES_DATASET?.trim() || "";const configuredRelayTracingToken = repoEnv.VITE_RELAY_OTLP_TRACES_TOKEN?.trim() || "";const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || "";const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version;const configuredHostedAppUrl = (() => {  const explicitHostedAppUrl = process.env.VITE_HOSTED_APP_URL?.trim();  if (explicitHostedAppUrl) {    return explicitHostedAppUrl;  }  if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) {    return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;  }  if (process.env.VERCEL_URL) {    return `https://${process.env.VERCEL_URL}`;  }  return undefined;})();const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); // Vite 8.1's experimental bundled dev mode: serves rolldown-bundled chunks in// dev for much faster startup/reload on large module graphs, with HMR served// as hot patches. Opt-in while experimental: T3CODE_BUNDLED_DEV=1 pnpm dev:web// The dev runner defaults this on for --share runs (remote browsers pay a// round trip per import level in unbundled dev); T3CODE_BUNDLED_DEV=0 opts out.const bundledDevEnv = process.env.T3CODE_BUNDLED_DEV?.trim().toLowerCase();const bundledDev = bundledDevEnv === "1" || bundledDevEnv === "true"; const buildSourcemap: boolean | "hidden" =  sourcemapEnv === "0" || sourcemapEnv === "false"    ? false    : sourcemapEnv === "hidden"      ? "hidden"      : true; const unitTestProject = {  extends: true,  test: {    name: "unit",    include: ["src/**/*.test.{ts,tsx}"],    // The web runtime suite exercises auth bootstrap, saved environments,    // and websocket subscription lifecycles. Under the full monorepo test    // run, those async tests can exceed Vitest's default 5s budget.    hookTimeout: 15_000,    testTimeout: 15_000,    setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"],  },} satisfies TestProjectInlineConfiguration; function resolveDevProxyTarget(  backendPort: string | undefined,  wsUrl: string | undefined,): string | undefined {  // Browser dev is single-origin: the backend port is proxied through this  // server so the app works from any origin (localhost, tailnet, LAN, phone).  // T3CODE_PORT is set by scripts/dev-runner.ts for every non-desktop mode.  const port = Number(backendPort?.trim());  if (Number.isInteger(port) && port > 0) {    return `http://localhost:${port}/`;  }   // dev:desktop still points the renderer straight at the backend, so fall  // back to deriving the target from the explicit websocket URL.  if (!wsUrl) {    return undefined;  }   try {    const url = new URL(wsUrl);    if (url.protocol === "ws:") {      url.protocol = "http:";    } else if (url.protocol === "wss:") {      url.protocol = "https:";    }    url.pathname = "";    url.search = "";    url.hash = "";    return url.toString();  } catch {    return undefined;  }} const devProxyTarget = resolveDevProxyTarget(process.env.T3CODE_PORT, configuredWsUrl); // Vite's dev server sends JS uncompressed. On localhost that is free; over a// shared origin (tailnet, LAN) it is the whole cold-start: bundled dev serves// one ~25 MB chunk, and a typical uplink moves that in about a minute while// both machines sit idle. Compressing turns it into a few seconds of CPU.// Brotli quality 5 keeps encode time in the hundreds of ms; the default// (quality 11) would trade the transfer stall for an equally long encode stall.function devCompressionPlugin(): Plugin {  return {    name: "t3code:dev-compression",    apply: "serve",    configureServer(server) {      // compression() is typed against Express's req/res, which extend the      // node http objects Connect actually passes — safe to narrow.      server.middlewares.use(        compression({          brotli: { params: { [NodeZlib.constants.BROTLI_PARAM_QUALITY]: 5 } },        }) as unknown as Connect.NextHandleFunction,      );    },  };} // Vite rejects requests whose Host header isn't localhost, which blocks sharing// a dev server over Tailscale/LAN. Tailnet names are safe to allow wholesale:// the DNS is controlled by tailscale, so they can't be rebound by an attacker.// Anything else (ngrok, a LAN IP alias) goes through the env var.const configuredAllowedHosts = (process.env.T3CODE_DEV_ALLOWED_HOSTS ?? "")  .split(",")  .map((entry) => entry.trim())  .filter((entry) => entry.length > 0);const allowedHosts = [".ts.net", ...configuredAllowedHosts]; export default defineConfig(() => {  return {    assetsInclude: ["**/*.wasm"],    plugins: [      devCompressionPlugin(),      // Route components load as split chunks so settings, pull-request, and      // usage code stay out of the cold-start payload; the router prefetches      // them on navigation intent (see getRouter's defaultPreload).      tanstackRouter({ autoCodeSplitting: true }),      react(),      babel({        // We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0        // This is because the babel plugin only automatically parses typescript and jsx based on relative paths (e.g. "**/*.ts")        // whereas the previous version of the plugin parsed all files with a .ts extension.        // This is causing our packages/ directory to fail to parse, as they are not relative to the CWD.        parserOpts: { plugins: ["typescript", "jsx"] },        presets: [reactCompilerPreset()],      }),      tailwindPlugins(bundledDev),    ],    optimizeDeps: {      include: [        "@clerk/clerk-js",        "@clerk/react/internal",        "@pierre/diffs",        "@pierre/diffs/editor",        "@pierre/diffs/react",        "@pierre/diffs/worker/worker.js",        "effect/Array",        "effect/Order",        "react-dom/client",      ],    },    define: {      // In dev mode, tell the web app where the WebSocket server lives      "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),      // Pinned explicitly rather than left to Vite's automatic VITE_ exposure:      // under single-origin dev this must stay empty even when a `.env`      // supplies it, so the client falls back to window.location.origin.      "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""),      "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl),      "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey),      "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate),      "import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID": JSON.stringify(        configuredClerkCliOAuthClientId,      ),      "import.meta.env.VITE_RELAY_OTLP_TRACES_URL": JSON.stringify(configuredRelayTracingUrl),      "import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET": JSON.stringify(        configuredRelayTracingDataset,      ),      "import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN": JSON.stringify(configuredRelayTracingToken),      "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),      "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel),      "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion),    },    resolve: {      tsconfigPaths: true,      dedupe: ["react", "react-dom"],    },    experimental: {      bundledDev,    },    server: {      host,      port,      strictPort: true,      allowedHosts,      // Transform the whole module graph at server start instead of on the      // first request. Without this, a cold worktree discovers and transforms      // modules one import-level at a time while the browser waits — which      // over a tailnet origin turns into minutes of waterfall.      warmup: {        clientFiles: ["./src/main.tsx"],      },      ...(devProxyTarget        ? {            // One entry per shared prefix; the server's dev catch-all 404s the            // same list, so the two sides cannot drift. `/ws` is the app's own            // socket and `/api` carries the device hub's stream sockets —            // Vite's HMR socket is matched separately and exactly (path "/"            // plus a vite-hmr subprotocol), so the upgrade handlers don't            // collide.            proxy: Object.fromEntries(              DEV_PROXIED_PATH_PREFIXES.map((prefix) => [                prefix,                {                  target: devProxyTarget,                  changeOrigin: true,                  ...(prefix === "/ws" || prefix === "/api" ? { ws: true } : {}),                },              ]),            ),          }        : {}),      // Electron's BrowserWindow needs the HMR socket pinned to an explicit      // host to connect reliably; dev:desktop is the only mode that sets HOST.      // Everywhere else, leaving this unset lets the client derive it from the      // page origin, which is what makes HMR work over Tailscale/LAN instead of      // failing an attempt against the wrong machine's localhost first.      // (Vite 8 logs connection state via console.debug — enable "Verbose".)      ...(explicitHost        ? {            hmr: {              protocol: "ws",              host: explicitHost,              clientPort: port,            },          }        : {}),    },    // @tailwindcss/vite only emits a CSS sourcemap when devSourcemap is on; without it    // rolldown flags the transform as SOURCEMAP_BROKEN on every sourcemapped build.    css: {      devSourcemap: buildSourcemap !== false,    },    build: {      outDir: "dist",      emptyOutDir: true,      manifest: true,      sourcemap: buildSourcemap,    },    test: {      projects: [defineProject(unitTestProject)],    },  };});