mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
293 lines
12 KiB
TypeScript
293 lines
12 KiB
TypeScript
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_ORIGIN_PRESERVING_PREFIXES,
|
|
DEV_PROXIED_PATH_PREFIXES,
|
|
} from "@t3tools/shared/devProxy";
|
|
|
|
import { loadRepoEnv } from "../../scripts/lib/public-config";
|
|
import { thirdPartyLicensesPlugin } from "../../scripts/lib/third-party-licenses";
|
|
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(),
|
|
thirdPartyLicensesPlugin({
|
|
bundleName: "web",
|
|
configFile: new URL("../../third-party-licenses.config.json", import.meta.url),
|
|
packageManifests: [
|
|
{ bundle: "web", path: new URL("./package.json", import.meta.url) },
|
|
{ bundle: "server", path: new URL("../server/package.json", import.meta.url) },
|
|
{ bundle: "desktop", path: new URL("../desktop/package.json", import.meta.url) },
|
|
],
|
|
}),
|
|
// 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/edit",
|
|
"@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: !DEV_PROXIED_ORIGIN_PRESERVING_PREFIXES.has(prefix),
|
|
...(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)],
|
|
},
|
|
};
|
|
});
|