authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 05:28:00-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 02:28:00-07:00
log388b43a27cc37ae067087073825633d4fc5de3b7
treeb2a230e5172d87e38440c10d0485c4d7906c0e8a
parent23f0a1ae38cb1fc510a499ef2b3602f5ae98d0c9
signature Signed by PGP key B5690EEEBB952194

perf(dev): faster cold-start for dev web serving (#5584)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

9 files changed, 267 insertions(+), 3 deletions(-)

apps/web/package.json+2
...@@ -52,11 +52,13 @@...@@ -52,11 +52,13 @@
52 "@tailwindcss/vite": "^4.0.0",52 "@tailwindcss/vite": "^4.0.0",
53 "@tanstack/router-plugin": "^1.161.0",53 "@tanstack/router-plugin": "^1.161.0",
54 "@types/babel__core": "^7.20.5",54 "@types/babel__core": "^7.20.5",
55 "@types/compression": "^1.8.1",
55 "@types/react": "~19.2.14",56 "@types/react": "~19.2.14",
56 "@types/react-dom": "~19.2.3",57 "@types/react-dom": "~19.2.3",
57 "@vercel/config": "^0.3.0",58 "@vercel/config": "^0.3.0",
58 "@vitejs/plugin-react": "^6.0.0",59 "@vitejs/plugin-react": "^6.0.0",
59 "babel-plugin-react-compiler": "1.0.0",60 "babel-plugin-react-compiler": "1.0.0",
61 "compression": "^1.8.1",
60 "msw": "2.12.11",62 "msw": "2.12.11",
61 "tailwindcss": "^4.0.0",63 "tailwindcss": "^4.0.0",
62 "vite": "catalog:",64 "vite": "catalog:",
apps/web/scripts/warm-dep-cache.ts created+22
...@@ -0,0 +1,22 @@
1// @effect-diagnostics nodeBuiltinImport:off - setup-script bootstrap, runs before any Effect runtime exists.
2/**
3 * Pre-warms Vite's dependency-optimizer cache (`node_modules/.vite/deps`) so
4 * the first `vp run dev` in a fresh worktree doesn't stall the initial page
5 * load on a full optimize pass. Run by the t3.json worktree setup script;
6 * safe to re-run — a valid cache makes this a fast no-op.
7 *
8 * The cache cannot be shared between worktrees: Vite's config hash includes
9 * the absolute project root, so each worktree must warm its own.
10 */
11import * as NodePath from "node:path";
12import * as NodeURL from "node:url";
13
14import { optimizeDeps, resolveConfig } from "vite";
15
16const webRoot = NodePath.dirname(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)));
17
18// logLevel "error" silences the "manually calling optimizeDeps is deprecated"
19// warning — deliberate here: warming ahead of the server is the whole point.
20const config = await resolveConfig({ root: webRoot, logLevel: "error" }, "serve");
21await optimizeDeps(config);
22console.log("[warm-dep-cache] web dependency cache is warm");
apps/web/tsconfig.json+8-1
...@@ -25,5 +25,12 @@...@@ -25,5 +25,12 @@
25 }25 }
26 ]26 ]
27 },27 },
28 "include": ["src", "vite.config.ts", "vercel.ts", "test", "../../scripts/lib/public-config.ts"]28 "include": [
29 "src",
30 "vite.config.ts",
31 "vercel.ts",
32 "test",
33 "scripts/warm-dep-cache.ts",
34 "../../scripts/lib/public-config.ts"
35 ]
29}36}
apps/web/vite.config.ts+36-1
...@@ -1,10 +1,13 @@...@@ -1,10 +1,13 @@
1import * as NodeZlib from "node:zlib";
2
1import tailwindcss from "@tailwindcss/vite";3import tailwindcss from "@tailwindcss/vite";
2import react, { reactCompilerPreset } from "@vitejs/plugin-react";4import react, { reactCompilerPreset } from "@vitejs/plugin-react";
3import babel from "@rolldown/plugin-babel";5import babel from "@rolldown/plugin-babel";
4import { tanstackRouter } from "@tanstack/router-plugin/vite";6import { tanstackRouter } from "@tanstack/router-plugin/vite";
7import compression from "compression";
5import { defineProject, type TestProjectInlineConfiguration } from "vite-plus/test/config";8import { defineProject, type TestProjectInlineConfiguration } from "vite-plus/test/config";
6import "vite-plus/test/config";9import "vite-plus/test/config";
7import { defineConfig } from "vite-plus";10import { defineConfig, type Connect, type Plugin } from "vite-plus";
8import pkg from "./package.json" with { type: "json" };11import pkg from "./package.json" with { type: "json" };
912
10import { DEV_PROXIED_PATH_PREFIXES } from "@t3tools/shared/devProxy";13import { DEV_PROXIED_PATH_PREFIXES } from "@t3tools/shared/devProxy";
...@@ -55,6 +58,8 @@ const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase();...@@ -55,6 +58,8 @@ const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase();
55// Vite 8.1's experimental bundled dev mode: serves rolldown-bundled chunks in58// Vite 8.1's experimental bundled dev mode: serves rolldown-bundled chunks in
56// dev for much faster startup/reload on large module graphs, with HMR served59// dev for much faster startup/reload on large module graphs, with HMR served
57// as hot patches. Opt-in while experimental: T3CODE_BUNDLED_DEV=1 pnpm dev:web60// as hot patches. Opt-in while experimental: T3CODE_BUNDLED_DEV=1 pnpm dev:web
61// The dev runner defaults this on for --share runs (remote browsers pay a
62// round trip per import level in unbundled dev); T3CODE_BUNDLED_DEV=0 opts out.
58const bundledDevEnv = process.env.T3CODE_BUNDLED_DEV?.trim().toLowerCase();63const bundledDevEnv = process.env.T3CODE_BUNDLED_DEV?.trim().toLowerCase();
59const bundledDev = bundledDevEnv === "1" || bundledDevEnv === "true";64const bundledDev = bundledDevEnv === "1" || bundledDevEnv === "true";
6065
...@@ -114,6 +119,28 @@ function resolveDevProxyTarget(...@@ -114,6 +119,28 @@ function resolveDevProxyTarget(
114119
115const devProxyTarget = resolveDevProxyTarget(process.env.T3CODE_PORT, configuredWsUrl);120const devProxyTarget = resolveDevProxyTarget(process.env.T3CODE_PORT, configuredWsUrl);
116121
122// Vite's dev server sends JS uncompressed. On localhost that is free; over a
123// shared origin (tailnet, LAN) it is the whole cold-start: bundled dev serves
124// one ~25 MB chunk, and a typical uplink moves that in about a minute while
125// both machines sit idle. Compressing turns it into a few seconds of CPU.
126// Brotli quality 5 keeps encode time in the hundreds of ms; the default
127// (quality 11) would trade the transfer stall for an equally long encode stall.
128function devCompressionPlugin(): Plugin {
129 return {
130 name: "t3code:dev-compression",
131 apply: "serve",
132 configureServer(server) {
133 // compression() is typed against Express's req/res, which extend the
134 // node http objects Connect actually passes — safe to narrow.
135 server.middlewares.use(
136 compression({
137 brotli: { params: { [NodeZlib.constants.BROTLI_PARAM_QUALITY]: 5 } },
138 }) as unknown as Connect.NextHandleFunction,
139 );
140 },
141 };
142}
143
117// Vite rejects requests whose Host header isn't localhost, which blocks sharing144// Vite rejects requests whose Host header isn't localhost, which blocks sharing
118// a dev server over Tailscale/LAN. Tailnet names are safe to allow wholesale:145// a dev server over Tailscale/LAN. Tailnet names are safe to allow wholesale:
119// the DNS is controlled by tailscale, so they can't be rebound by an attacker.146// the DNS is controlled by tailscale, so they can't be rebound by an attacker.
...@@ -128,6 +155,7 @@ export default defineConfig(() => {...@@ -128,6 +155,7 @@ export default defineConfig(() => {
128 return {155 return {
129 assetsInclude: ["**/*.wasm"],156 assetsInclude: ["**/*.wasm"],
130 plugins: [157 plugins: [
158 devCompressionPlugin(),
131 tanstackRouter(),159 tanstackRouter(),
132 react(),160 react(),
133 babel({161 babel({
...@@ -187,6 +215,13 @@ export default defineConfig(() => {...@@ -187,6 +215,13 @@ export default defineConfig(() => {
187 port,215 port,
188 strictPort: true,216 strictPort: true,
189 allowedHosts,217 allowedHosts,
218 // Transform the whole module graph at server start instead of on the
219 // first request. Without this, a cold worktree discovers and transforms
220 // modules one import-level at a time while the browser waits — which
221 // over a tailnet origin turns into minutes of waterfall.
222 warmup: {
223 clientFiles: ["./src/main.tsx"],
224 },
190 ...(devProxyTarget225 ...(devProxyTarget
191 ? {226 ? {
192 // One entry per shared prefix; the server's dev catch-all 404s the227 // One entry per shared prefix; the server's dev catch-all 404s the
docs/internals/scripts.md+3
...@@ -24,6 +24,9 @@ authenticated....@@ -24,6 +24,9 @@ authenticated.
24- `vp run dev`: Starts contracts, server, and web in watch mode.24- `vp run dev`: Starts contracts, server, and web in watch mode.
25- `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The25- `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The
26 startup pairing URL is built against the shared origin, and the mapping is removed on exit.26 startup pairing URL is built against the shared origin, and the mapping is removed on exit.
27 Shared runs default to Vite's bundled dev mode (`T3CODE_BUNDLED_DEV=1`): a remote browser pays a
28 network round trip per import level in unbundled dev, which turns a cold module graph into
29 minutes of waterfall. Set `T3CODE_BUNDLED_DEV=0` to opt a shared run back out.
27- `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes30- `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes
28 `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has31 `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has
29 no effect; use `--browser`.32 no effect; use `--browser`.
pnpm-lock.yaml+78
...@@ -623,6 +623,9 @@ importers:...@@ -623,6 +623,9 @@ importers:
623 '@types/babel__core':623 '@types/babel__core':
624 specifier: ^7.20.5624 specifier: ^7.20.5
625 version: 7.20.5625 version: 7.20.5
626 '@types/compression':
627 specifier: ^1.8.1
628 version: 1.8.1
626 '@types/react':629 '@types/react':
627 specifier: ~19.2.14630 specifier: ~19.2.14
628 version: 19.2.16631 version: 19.2.16
...@@ -638,6 +641,9 @@ importers:...@@ -638,6 +641,9 @@ importers:
638 babel-plugin-react-compiler:641 babel-plugin-react-compiler:
639 specifier: 1.0.0642 specifier: 1.0.0
640 version: 1.0.0643 version: 1.0.0
644 compression:
645 specifier: ^1.8.1
646 version: 1.8.1
641 msw:647 msw:
642 specifier: 2.12.11648 specifier: 2.12.11
643 version: 2.12.11(@types/node@24.12.4)(typescript@6.0.3)649 version: 2.12.11(@types/node@24.12.4)(typescript@6.0.3)
...@@ -4777,6 +4783,9 @@ packages:...@@ -4777,6 +4783,9 @@ packages:
4777 '@types/babel__traverse@7.28.0':4783 '@types/babel__traverse@7.28.0':
4778 resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}4784 resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
47794785
4786 '@types/body-parser@1.19.6':
4787 resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
4788
4780 '@types/bun@1.3.14':4789 '@types/bun@1.3.14':
4781 resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==}4790 resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==}
47824791
...@@ -4786,6 +4795,12 @@ packages:...@@ -4786,6 +4795,12 @@ packages:
4786 '@types/chai@5.2.3':4795 '@types/chai@5.2.3':
4787 resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}4796 resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
47884797
4798 '@types/compression@1.8.1':
4799 resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==}
4800
4801 '@types/connect@3.4.38':
4802 resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
4803
4789 '@types/debug@4.1.13':4804 '@types/debug@4.1.13':
4790 resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}4805 resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
47914806
...@@ -4801,6 +4816,12 @@ packages:...@@ -4801,6 +4816,12 @@ packages:
4801 '@types/estree@1.0.9':4816 '@types/estree@1.0.9':
4802 resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}4817 resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
48034818
4819 '@types/express-serve-static-core@5.1.3':
4820 resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==}
4821
4822 '@types/express@5.0.6':
4823 resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
4824
4804 '@types/fs-extra@9.0.13':4825 '@types/fs-extra@9.0.13':
4805 resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}4826 resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
48064827
...@@ -4813,6 +4834,9 @@ packages:...@@ -4813,6 +4834,9 @@ packages:
4813 '@types/http-cache-semantics@4.2.0':4834 '@types/http-cache-semantics@4.2.0':
4814 resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==}4835 resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==}
48154836
4837 '@types/http-errors@2.0.5':
4838 resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
4839
4816 '@types/istanbul-lib-coverage@2.0.6':4840 '@types/istanbul-lib-coverage@2.0.6':
4817 resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}4841 resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
48184842
...@@ -4843,6 +4867,12 @@ packages:...@@ -4843,6 +4867,12 @@ packages:
4843 '@types/pngjs@6.0.5':4867 '@types/pngjs@6.0.5':
4844 resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==}4868 resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==}
48454869
4870 '@types/qs@6.15.1':
4871 resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
4872
4873 '@types/range-parser@1.2.7':
4874 resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
4875
4846 '@types/react-dom@19.2.3':4876 '@types/react-dom@19.2.3':
4847 resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}4877 resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
4848 peerDependencies:4878 peerDependencies:
...@@ -4857,6 +4887,12 @@ packages:...@@ -4857,6 +4887,12 @@ packages:
4857 '@types/responselike@1.0.3':4887 '@types/responselike@1.0.3':
4858 resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}4888 resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
48594889
4890 '@types/send@1.2.1':
4891 resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
4892
4893 '@types/serve-static@2.2.0':
4894 resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
4895
4860 '@types/statuses@2.0.6':4896 '@types/statuses@2.0.6':
4861 resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}4897 resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
48624898
...@@ -14913,6 +14949,11 @@ snapshots:...@@ -14913,6 +14949,11 @@ snapshots:
14913 dependencies:14949 dependencies:
14914 '@babel/types': 7.29.714950 '@babel/types': 7.29.7
1491514951
14952 '@types/body-parser@1.19.6':
14953 dependencies:
14954 '@types/connect': 3.4.38
14955 '@types/node': 24.12.4
14956
14916 '@types/bun@1.3.14':14957 '@types/bun@1.3.14':
14917 dependencies:14958 dependencies:
14918 bun-types: 1.3.1414959 bun-types: 1.3.14
...@@ -14929,6 +14970,15 @@ snapshots:...@@ -14929,6 +14970,15 @@ snapshots:
14929 '@types/deep-eql': 4.0.214970 '@types/deep-eql': 4.0.2
14930 assertion-error: 2.0.114971 assertion-error: 2.0.1
1493114972
14973 '@types/compression@1.8.1':
14974 dependencies:
14975 '@types/express': 5.0.6
14976 '@types/node': 24.12.4
14977
14978 '@types/connect@3.4.38':
14979 dependencies:
14980 '@types/node': 24.12.4
14981
14932 '@types/debug@4.1.13':14982 '@types/debug@4.1.13':
14933 dependencies:14983 dependencies:
14934 '@types/ms': 2.1.014984 '@types/ms': 2.1.0
...@@ -14943,6 +14993,19 @@ snapshots:...@@ -14943,6 +14993,19 @@ snapshots:
1494314993
14944 '@types/estree@1.0.9': {}14994 '@types/estree@1.0.9': {}
1494514995
14996 '@types/express-serve-static-core@5.1.3':
14997 dependencies:
14998 '@types/node': 24.12.4
14999 '@types/qs': 6.15.1
15000 '@types/range-parser': 1.2.7
15001 '@types/send': 1.2.1
15002
15003 '@types/express@5.0.6':
15004 dependencies:
15005 '@types/body-parser': 1.19.6
15006 '@types/express-serve-static-core': 5.1.3
15007 '@types/serve-static': 2.2.0
15008
14946 '@types/fs-extra@9.0.13':15009 '@types/fs-extra@9.0.13':
14947 dependencies:15010 dependencies:
14948 '@types/node': 24.12.415011 '@types/node': 24.12.4
...@@ -14955,6 +15018,8 @@ snapshots:...@@ -14955,6 +15018,8 @@ snapshots:
1495515018
14956 '@types/http-cache-semantics@4.2.0': {}15019 '@types/http-cache-semantics@4.2.0': {}
1495715020
15021 '@types/http-errors@2.0.5': {}
15022
14958 '@types/istanbul-lib-coverage@2.0.6': {}15023 '@types/istanbul-lib-coverage@2.0.6': {}
1495915024
14960 '@types/istanbul-lib-report@3.0.3':15025 '@types/istanbul-lib-report@3.0.3':
...@@ -14989,6 +15054,10 @@ snapshots:...@@ -14989,6 +15054,10 @@ snapshots:
14989 dependencies:15054 dependencies:
14990 '@types/node': 24.12.415055 '@types/node': 24.12.4
1499115056
15057 '@types/qs@6.15.1': {}
15058
15059 '@types/range-parser@1.2.7': {}
15060
14992 '@types/react-dom@19.2.3(@types/react@19.2.16)':15061 '@types/react-dom@19.2.3(@types/react@19.2.16)':
14993 dependencies:15062 dependencies:
14994 '@types/react': 19.2.1615063 '@types/react': 19.2.16
...@@ -15005,6 +15074,15 @@ snapshots:...@@ -15005,6 +15074,15 @@ snapshots:
15005 dependencies:15074 dependencies:
15006 '@types/node': 24.12.415075 '@types/node': 24.12.4
1500715076
15077 '@types/send@1.2.1':
15078 dependencies:
15079 '@types/node': 24.12.4
15080
15081 '@types/serve-static@2.2.0':
15082 dependencies:
15083 '@types/http-errors': 2.0.5
15084 '@types/node': 24.12.4
15085
15008 '@types/statuses@2.0.6': {}15086 '@types/statuses@2.0.6': {}
1500915087
15010 '@types/unist@2.0.11': {}15088 '@types/unist@2.0.11': {}
scripts/dev-runner.test.ts+109
...@@ -986,6 +986,115 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -986,6 +986,115 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
986 });986 });
987 });987 });
988988
989 // A shared origin means a remote browser, where unbundled dev's
990 // per-module waterfall pays a tailnet round trip per import level. The
991 // runner defaults bundled dev on for the spawned stack, but only
992 // defaults: an explicit T3CODE_BUNDLED_DEV (even "0") must pass through.
993 describe("--share bundled dev default", () => {
994 const shareSpawnedEnv = (input: { readonly ambientBundledDev: string | undefined }) =>
995 Effect.gen(function* () {
996 let captured: Record<string, string | undefined> | undefined;
997 const spawnerLayer = Layer.succeed(
998 ChildProcessSpawner.ChildProcessSpawner,
999 ChildProcessSpawner.make((command) => {
1000 const spawned = command as unknown as {
1001 readonly command: string;
1002 readonly args: ReadonlyArray<string>;
1003 readonly options?: { readonly env?: Record<string, string | undefined> };
1004 };
1005 if (spawned.command === "vp") {
1006 captured = spawned.options?.env;
1007 return Effect.succeed(mockProcess(0));
1008 }
1009 // tailscale: answer `status --json` with a valid tailnet name,
1010 // succeed the `serve`/`off` calls.
1011 return Effect.succeed(
1012 ChildProcessSpawner.makeHandle({
1013 pid: ChildProcessSpawner.ProcessId(2),
1014 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
1015 isRunning: Effect.succeed(false),
1016 kill: () => Effect.void,
1017 unref: Effect.succeed(Effect.void),
1018 stdin: Sink.drain,
1019 stdout: spawned.args.includes("status")
1020 ? Stream.make(
1021 new TextEncoder().encode(
1022 JSON.stringify({ Self: { DNSName: "host.example.ts.net." } }),
1023 ),
1024 )
1025 : Stream.empty,
1026 stderr: Stream.empty,
1027 all: Stream.empty,
1028 getInputFd: () => Sink.drain,
1029 getOutputFd: () => Stream.empty,
1030 }),
1031 );
1032 }),
1033 );
1034
1035 yield* runDevRunnerWithInput({
1036 ...devServerInput,
1037 mode: "dev",
1038 port: undefined,
1039 share: true,
1040 }).pipe(
1041 Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)),
1042 Effect.provideService(HostProcessPlatform, "linux"),
1043 Effect.provideService(
1044 HostProcessEnvironment,
1045 input.ambientBundledDev === undefined
1046 ? {}
1047 : { T3CODE_BUNDLED_DEV: input.ambientBundledDev },
1048 ),
1049 );
1050
1051 return captured;
1052 });
1053
1054 it.effect("defaults T3CODE_BUNDLED_DEV=1 for a shared run", () =>
1055 Effect.gen(function* () {
1056 const env = yield* shareSpawnedEnv({ ambientBundledDev: undefined });
1057 assert.equal(env?.T3CODE_BUNDLED_DEV, "1");
1058 }),
1059 );
1060
1061 it.effect("keeps an explicit T3CODE_BUNDLED_DEV=0 opt-out", () =>
1062 Effect.gen(function* () {
1063 const env = yield* shareSpawnedEnv({ ambientBundledDev: "0" });
1064 assert.equal(env?.T3CODE_BUNDLED_DEV, "0");
1065 }),
1066 );
1067
1068 it.effect("leaves T3CODE_BUNDLED_DEV unset without --share", () =>
1069 Effect.gen(function* () {
1070 let captured: Record<string, string | undefined> | undefined;
1071 const spawnerLayer = Layer.succeed(
1072 ChildProcessSpawner.ChildProcessSpawner,
1073 ChildProcessSpawner.make((command) => {
1074 captured = (
1075 command as {
1076 readonly options?: { readonly env?: Record<string, string | undefined> };
1077 }
1078 ).options?.env;
1079 return Effect.succeed(mockProcess(0));
1080 }),
1081 );
1082
1083 yield* runDevRunnerWithInput({
1084 ...devServerInput,
1085 mode: "dev",
1086 port: undefined,
1087 }).pipe(
1088 Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)),
1089 Effect.provideService(HostProcessPlatform, "linux"),
1090 Effect.provideService(HostProcessEnvironment, {}),
1091 );
1092
1093 assert.equal(captured?.T3CODE_BUNDLED_DEV, undefined);
1094 }),
1095 );
1096 });
1097
989 it.effect("spawns nothing when --dry-run is combined with --share", () => {1098 it.effect("spawns nothing when --dry-run is combined with --share", () => {
990 let spawnCount = 0;1099 let spawnCount = 0;
991 const spawnerLayer = Layer.succeed(1100 const spawnerLayer = Layer.succeed(
scripts/dev-runner.ts+8
...@@ -780,6 +780,14 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {...@@ -780,6 +780,14 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {
780 if (input.devUrl === undefined) {780 if (input.devUrl === undefined) {
781 env.VITE_DEV_SERVER_URL = shared.url;781 env.VITE_DEV_SERVER_URL = shared.url;
782 }782 }
783 // A shared origin serves a remote browser, where unbundled dev's
784 // per-module requests each pay a tailnet round trip — a cold module
785 // graph takes minutes to first paint. Bundled dev collapses that to
786 // a few chunk requests. Only defaulted, so T3CODE_BUNDLED_DEV=0
787 // still opts a --share run back out.
788 if (env.T3CODE_BUNDLED_DEV === undefined) {
789 env.T3CODE_BUNDLED_DEV = "1";
790 }
783 yield* Effect.logInfo(`[dev-runner] shared on tailnet: ${shared.url}`);791 yield* Effect.logInfo(`[dev-runner] shared on tailnet: ${shared.url}`);
784 }792 }
785 }793 }
t3.json+1-1
...@@ -4,7 +4,7 @@...@@ -4,7 +4,7 @@
4 "scripts": [4 "scripts": [
5 {5 {
6 "name": "Setup Worktree",6 "name": "Setup Worktree",
7 "command": "vp i && ln -sf $T3CODE_PROJECT_ROOT/.env .env && ln -sf $T3CODE_PROJECT_ROOT/infra/relay/.env infra/relay/.env",7 "command": "vp i && ln -sf $T3CODE_PROJECT_ROOT/.env .env && ln -sf $T3CODE_PROJECT_ROOT/infra/relay/.env infra/relay/.env && node apps/web/scripts/warm-dep-cache.ts",
8 "icon": "configure",8 "icon": "configure",
9 "runOnWorktreeCreate": true9 "runOnWorktreeCreate": true
10 }10 }