From 1e7f472cc3e9cf6d100741e22eacaa1acfa787cc Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 01:18:37 -0700 Subject: [PATCH 1/3] perf(dev): faster cold-start for dev web serving - --share defaults to Vite's bundled dev mode so remote browsers don't pay a tailnet round trip per import level (T3CODE_BUNDLED_DEV=0 opts out) - server.warmup transforms the module graph at startup instead of on the first request - worktree setup now pre-warms the dependency-optimizer cache, so the first dev start skips the optimize pass Co-Authored-By: Claude Fable 5 --- apps/web/scripts/warm-dep-cache.ts | 22 ++++++ apps/web/tsconfig.json | 9 ++- apps/web/vite.config.ts | 9 +++ docs/internals/scripts.md | 3 + scripts/dev-runner.test.ts | 109 +++++++++++++++++++++++++++++ scripts/dev-runner.ts | 8 +++ t3.json | 2 +- 7 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 apps/web/scripts/warm-dep-cache.ts diff --git a/apps/web/scripts/warm-dep-cache.ts b/apps/web/scripts/warm-dep-cache.ts new file mode 100644 index 00000000000..4fe50a3bf9c --- /dev/null +++ b/apps/web/scripts/warm-dep-cache.ts @@ -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"); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 0e226fc136d..186b6ebfe50 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -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" + ] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 24d76c2d9f3..b86556f1fbc 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -55,6 +55,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"; @@ -187,6 +189,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 diff --git a/docs/internals/scripts.md b/docs/internals/scripts.md index 2a020701064..9440115e1a9 100644 --- a/docs/internals/scripts.md +++ b/docs/internals/scripts.md @@ -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`. diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 2ea3064c2a4..17be2c6263f 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -986,6 +986,115 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }); }); + // A shared origin means a remote browser, where unbundled dev's + // per-module waterfall pays a tailnet round trip per import level. The + // runner defaults bundled dev on for the spawned stack, but only + // defaults: an explicit T3CODE_BUNDLED_DEV (even "0") must pass through. + describe("--share bundled dev default", () => { + const shareSpawnedEnv = (input: { readonly ambientBundledDev: string | undefined }) => + Effect.gen(function* () { + let captured: Record | undefined; + const spawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => { + const spawned = command as unknown as { + readonly command: string; + readonly args: ReadonlyArray; + readonly options?: { readonly env?: Record }; + }; + if (spawned.command === "vp") { + captured = spawned.options?.env; + return Effect.succeed(mockProcess(0)); + } + // tailscale: answer `status --json` with a valid tailnet name, + // succeed the `serve`/`off` calls. + return Effect.succeed( + ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(2), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), + isRunning: Effect.succeed(false), + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + stdin: Sink.drain, + stdout: spawned.args.includes("status") + ? Stream.make( + new TextEncoder().encode( + JSON.stringify({ Self: { DNSName: "host.example.ts.net." } }), + ), + ) + : Stream.empty, + stderr: Stream.empty, + all: Stream.empty, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }), + ); + }), + ); + + yield* runDevRunnerWithInput({ + ...devServerInput, + mode: "dev", + port: undefined, + share: true, + }).pipe( + Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), + Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService( + HostProcessEnvironment, + input.ambientBundledDev === undefined + ? {} + : { T3CODE_BUNDLED_DEV: input.ambientBundledDev }, + ), + ); + + return captured; + }); + + it.effect("defaults T3CODE_BUNDLED_DEV=1 for a shared run", () => + Effect.gen(function* () { + const env = yield* shareSpawnedEnv({ ambientBundledDev: undefined }); + assert.equal(env?.T3CODE_BUNDLED_DEV, "1"); + }), + ); + + it.effect("keeps an explicit T3CODE_BUNDLED_DEV=0 opt-out", () => + Effect.gen(function* () { + const env = yield* shareSpawnedEnv({ ambientBundledDev: "0" }); + assert.equal(env?.T3CODE_BUNDLED_DEV, "0"); + }), + ); + + it.effect("leaves T3CODE_BUNDLED_DEV unset without --share", () => + Effect.gen(function* () { + let captured: Record | undefined; + const spawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => { + captured = ( + command as { + readonly options?: { readonly env?: Record }; + } + ).options?.env; + return Effect.succeed(mockProcess(0)); + }), + ); + + yield* runDevRunnerWithInput({ + ...devServerInput, + mode: "dev", + port: undefined, + }).pipe( + Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), + Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcessEnvironment, {}), + ); + + assert.equal(captured?.T3CODE_BUNDLED_DEV, undefined); + }), + ); + }); + it.effect("spawns nothing when --dry-run is combined with --share", () => { let spawnCount = 0; const spawnerLayer = Layer.succeed( diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index cb4e3f74c4f..6324d852bb1 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -780,6 +780,14 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { if (input.devUrl === undefined) { env.VITE_DEV_SERVER_URL = shared.url; } + // A shared origin serves a remote browser, where unbundled dev's + // per-module requests each pay a tailnet round trip — a cold module + // graph takes minutes to first paint. Bundled dev collapses that to + // a few chunk requests. Only defaulted, so T3CODE_BUNDLED_DEV=0 + // still opts a --share run back out. + if (env.T3CODE_BUNDLED_DEV === undefined) { + env.T3CODE_BUNDLED_DEV = "1"; + } yield* Effect.logInfo(`[dev-runner] shared on tailnet: ${shared.url}`); } } diff --git a/t3.json b/t3.json index 3a6956003af..007e8f96194 100644 --- a/t3.json +++ b/t3.json @@ -4,7 +4,7 @@ "scripts": [ { "name": "Setup Worktree", - "command": "vp i && ln -sf $T3CODE_PROJECT_ROOT/.env .env && ln -sf $T3CODE_PROJECT_ROOT/infra/relay/.env infra/relay/.env", + "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", "icon": "configure", "runOnWorktreeCreate": true } From 5fec72e84e94751c3b78ac5e31aba702189769af Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 02:07:49 -0700 Subject: [PATCH 2/3] perf(dev): compress dev-served JS for remote browsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev server sent JS uncompressed. Localhost never notices; a tailnet browser pays for it in full — bundled dev's ~25 MB chunk took ~55 s on a real uplink. Brotli (quality 5) moves the same chunk as ~4.3 MB, cutting cold load to seconds for a few hundred ms of encode time. Co-Authored-By: Claude Fable 5 --- apps/web/package.json | 2 ++ apps/web/vite.config.ts | 28 ++++++++++++++- pnpm-lock.yaml | 78 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 5b1789caee2..83f55abcfd5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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:", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index b86556f1fbc..155592fa8da 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,10 +1,13 @@ +import zlib 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"; @@ -116,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: { [zlib.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. @@ -130,6 +155,7 @@ export default defineConfig(() => { return { assetsInclude: ["**/*.wasm"], plugins: [ + devCompressionPlugin(), tanstackRouter(), react(), babel({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0be461aacbf..1eb0379e84a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,6 +623,9 @@ importers: '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 + '@types/compression': + specifier: ^1.8.1 + version: 1.8.1 '@types/react': specifier: ~19.2.14 version: 19.2.16 @@ -638,6 +641,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + compression: + specifier: ^1.8.1 + version: 1.8.1 msw: specifier: 2.12.11 version: 2.12.11(@types/node@24.12.4)(typescript@6.0.3) @@ -4777,6 +4783,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} @@ -4786,6 +4795,12 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/compression@1.8.1': + resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -4801,6 +4816,12 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -4813,6 +4834,9 @@ packages: '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4843,6 +4867,12 @@ packages: '@types/pngjs@6.0.5': resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4857,6 +4887,12 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -14913,6 +14949,11 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.12.4 + '@types/bun@1.3.14': dependencies: bun-types: 1.3.14 @@ -14929,6 +14970,15 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/compression@1.8.1': + dependencies: + '@types/express': 5.0.6 + '@types/node': 24.12.4 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.12.4 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -14943,6 +14993,19 @@ snapshots: '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 24.12.4 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 2.2.0 + '@types/fs-extra@9.0.13': dependencies: '@types/node': 24.12.4 @@ -14955,6 +15018,8 @@ snapshots: '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -14989,6 +15054,10 @@ snapshots: dependencies: '@types/node': 24.12.4 + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.16)': dependencies: '@types/react': 19.2.16 @@ -15005,6 +15074,15 @@ snapshots: dependencies: '@types/node': 24.12.4 + '@types/send@1.2.1': + dependencies: + '@types/node': 24.12.4 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.12.4 + '@types/statuses@2.0.6': {} '@types/unist@2.0.11': {} From 4c0dabd4487f88bb12e0b275b55eb6e6e6d21973 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 02:13:13 -0700 Subject: [PATCH 3/3] fix(web): satisfy namespace-node-imports lint in vite config Co-Authored-By: Claude Fable 5 --- apps/web/vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 155592fa8da..6ee5de587b9 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,4 +1,4 @@ -import zlib from "node:zlib"; +import * as NodeZlib from "node:zlib"; import tailwindcss from "@tailwindcss/vite"; import react, { reactCompilerPreset } from "@vitejs/plugin-react"; @@ -134,7 +134,7 @@ function devCompressionPlugin(): Plugin { // node http objects Connect actually passes — safe to narrow. server.middlewares.use( compression({ - brotli: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 } }, + brotli: { params: { [NodeZlib.constants.BROTLI_PARAM_QUALITY]: 5 } }, }) as unknown as Connect.NextHandleFunction, ); },