Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@
"@tailwindcss/vite": "^4.0.0",
"@tanstack/router-plugin": "^1.161.0",
"@types/babel__core": "^7.20.5",
"@types/compression": "^1.8.1",
"@types/react": "~19.2.14",
"@types/react-dom": "~19.2.3",
"@vercel/config": "^0.3.0",
"@vitejs/plugin-react": "^6.0.0",
"babel-plugin-react-compiler": "1.0.0",
"compression": "^1.8.1",
"msw": "2.12.11",
"tailwindcss": "^4.0.0",
"vite": "catalog:",
Expand Down
22 changes: 22 additions & 0 deletions apps/web/scripts/warm-dep-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @effect-diagnostics nodeBuiltinImport:off - setup-script bootstrap, runs before any Effect runtime exists.
/**
* Pre-warms Vite's dependency-optimizer cache (`node_modules/.vite/deps`) so
* the first `vp run dev` in a fresh worktree doesn't stall the initial page
* load on a full optimize pass. Run by the t3.json worktree setup script;
* safe to re-run — a valid cache makes this a fast no-op.
*
* The cache cannot be shared between worktrees: Vite's config hash includes
* the absolute project root, so each worktree must warm its own.
*/
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

import { optimizeDeps, resolveConfig } from "vite";

const webRoot = NodePath.dirname(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)));

// logLevel "error" silences the "manually calling optimizeDeps is deprecated"
// warning — deliberate here: warming ahead of the server is the whole point.
const config = await resolveConfig({ root: webRoot, logLevel: "error" }, "serve");
await optimizeDeps(config);
console.log("[warm-dep-cache] web dependency cache is warm");
9 changes: 8 additions & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,12 @@
}
]
},
"include": ["src", "vite.config.ts", "vercel.ts", "test", "../../scripts/lib/public-config.ts"]
"include": [
"src",
"vite.config.ts",
"vercel.ts",
"test",
"scripts/warm-dep-cache.ts",
"../../scripts/lib/public-config.ts"
]
}
37 changes: 36 additions & 1 deletion apps/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
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 } from "vite-plus";
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";
Expand Down Expand Up @@ -55,6 +58,8 @@ 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";

Expand Down Expand Up @@ -114,6 +119,28 @@ function resolveDevProxyTarget(

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.
Expand All @@ -128,6 +155,7 @@ export default defineConfig(() => {
return {
assetsInclude: ["**/*.wasm"],
plugins: [
devCompressionPlugin(),
tanstackRouter(),
react(),
babel({
Expand Down Expand Up @@ -187,6 +215,13 @@ export default defineConfig(() => {
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
Expand Down
3 changes: 3 additions & 0 deletions docs/internals/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ authenticated.
- `vp run dev`: Starts contracts, server, and web in watch mode.
- `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The
startup pairing URL is built against the shared origin, and the mapping is removed on exit.
Shared runs default to Vite's bundled dev mode (`T3CODE_BUNDLED_DEV=1`): a remote browser pays a
network round trip per import level in unbundled dev, which turns a cold module graph into
minutes of waterfall. Set `T3CODE_BUNDLED_DEV=0` to opt a shared run back out.
- `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes
`T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has
no effect; use `--browser`.
Expand Down
78 changes: 78 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading