import * as NodeZlib from "node:zlib"; import tailwindcss from "@tailwindcss/vite";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"; 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, },} 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(), tanstackRouter(), 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()], }), tailwindcss(), ], 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 — Vite's HMR socket is matched separately and exactly // (path "/" plus a vite-hmr subprotocol), so the two upgrade // handlers don't collide. proxy: Object.fromEntries( DEV_PROXIED_PATH_PREFIXES.map((prefix) => [ prefix, { target: devProxyTarget, changeOrigin: true, ...(prefix === "/ws" ? { 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, }, } : {}), }, build: { outDir: "dist", emptyOutDir: true, sourcemap: buildSourcemap, }, test: { projects: [defineProject(unitTestProject)], }, };});
import * as NodeZlib from "node:zlib";
import tailwindcss from "@tailwindcss/vite";
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";
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,
},
} 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(),
tanstackRouter(),
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()],
}),
tailwindcss(),
],
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 — Vite's HMR socket is matched separately and exactly
// (path "/" plus a vite-hmr subprotocol), so the two upgrade
// handlers don't collide.
proxy: Object.fromEntries(
DEV_PROXIED_PATH_PREFIXES.map((prefix) => [
prefix,
{
target: devProxyTarget,
changeOrigin: true,
...(prefix === "/ws" ? { 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,
},
}
: {}),
},
build: {
outDir: "dist",
emptyOutDir: true,
sourcemap: buildSourcemap,
},
test: {
projects: [defineProject(unitTestProject)],
},
};
});