Skip to content

Commit c8c2a19

Browse files
Enalmadaclaude
andcommitted
fix(e2e): hydration infinite-loop — drop broken use-sync-external-store polyfill, adopt gell-v2 optimizeDeps + PLAYWRIGHT env
Root cause (from Playwright trace artifacts on run 26543764735): console: 'The result of getServerSnapshot should be cached to avoid an infinite loop' console: 'Maximum update depth exceeded' console: 'Warning: Error in route match: __root__/' SSR rendered perfectly (the dehydrated HTML has <h1>Tasks</h1>, the session user, the empty task list — the whole shell). On the client, hydration hit React's infinite-loop guard and tore the DOM down, so 'getByText("Tasks")' resolved to undefined for every test. ALL 15 e2e tests failed for the same reason — not just the public-marketing subset that flaked under the earlier dep bump. The source: src/polyfills/use-sync-external-store-shim.ts (lines 19-23) returned 'selector(getSnapshot())' as a brand-new arrow function on every render. React's useSyncExternalStore reads the snapshot fn and detects the new closure as a snapshot change, which schedules a re-render, which creates another new closure, infinite loop. TanStack 1.149 happened not to trigger this path; 1.167 does. Three changes: 1. vite.config.ts: drop the resolve.alias block that pointed every 'use-sync-external-store/shim*' specifier at the broken polyfill. Vite resolves the real package (v1.6.0, already in node_modules, peerDeps include React 19) via optimizeDeps. src/polyfills/use-sync-external-store-shim.ts deleted — gell-v2 keeps an identical file as dead code but never aliases it; we delete instead to keep the polyfills/ dir honest. 2. vite.config.ts: optimizeDeps restructured to match gell-v2. - include: 8 deps Vite would otherwise discover on first request (TanStack history/router-core + helpers). Pre-bundling at startup prevents the 'optimized → reloading' mid-flight invalidation that crashes hydrating pages. - exclude: better-auth ecosystem (server-only by design — TSS-2 keeps them out of the client graph), jose / @noble/* / h3-v2 (server-only transitive deps). - force: true when PLAYWRIGHT=true (stale-hash references from a prior CI run can't break the first request). The previous include list shipped @better-auth/core/* + h3-v2 to the client optimizer pass — opposite direction from what TSS-2 demands. Caught here, but worth flagging as a near-miss. 3. playwright.config.ts: PLAYWRIGHT=true added to webServer.env so the optimizeDeps.force kicks in. vite.config.ts also reads it for server.hmr.overlay=false (the TanStack Start streaming-warning overlay otherwise intercepts Playwright clicks — TanStack/router#3584) and server.watch.ignored=['**/routeTree.gen.ts'] (Router's watchChange handler self-feeds otherwise). Quality gates: check-types ✓, biome ci ✓, check-tss-2 ✓ (8 createServerFn files clean), 62 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8a5656f commit c8c2a19

3 files changed

Lines changed: 54 additions & 86 deletions

File tree

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const config: PlaywrightTestConfig = {
5757
webServer: {
5858
reuseExistingServer: true,
5959
command:
60-
"bun run docker:up && sh scripts/wait-for.sh && bun run drizzle:migrate && cross-env GOOGLE_CLIENT_ID=test-client-id GOOGLE_CLIENT_SECRET=test-client-secret BETTER_AUTH_SECRET=test-auth-secret APP_ENV=development bun run dev:vite",
60+
"bun run docker:up && sh scripts/wait-for.sh && bun run drizzle:migrate && cross-env GOOGLE_CLIENT_ID=test-client-id GOOGLE_CLIENT_SECRET=test-client-secret BETTER_AUTH_SECRET=test-auth-secret APP_ENV=development PLAYWRIGHT=true bun run dev:vite",
6161
url: "http://localhost:3000",
6262
stdout: "pipe",
6363
stderr: "pipe",

src/polyfills/use-sync-external-store-shim.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

vite.config.ts

Lines changed: 53 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import path from "node:path";
21
import { lingui } from "@lingui/vite-plugin";
32
// TODO: Re-enable when Serwist Vite plugin works with Nitro v3
43
// import { serwist } from "@serwist/vite";
@@ -32,23 +31,25 @@ export default defineConfig({
3231
// Note: Warning about "native plugins disabled" is a known beta issue - can be ignored
3332
enableNativePlugin: true,
3433
},
34+
server: {
35+
hmr: {
36+
// PLAYWRIGHT env var is set in playwright.config.ts webServer.env.
37+
// The HMR error overlay intercepts pointer events and blocks
38+
// Playwright clicks when a transient SSR/streaming warning fires
39+
// (e.g. TanStack Start's "Response body object should not be
40+
// disturbed" — https://github.com/TanStack/router/issues/3584).
41+
// Keep the overlay on for human dev for visibility.
42+
overlay: process.env.PLAYWRIGHT !== "true",
43+
},
44+
watch: {
45+
// Prevent infinite watch loop: TanStack Router's `watchChange`
46+
// handler fires on its own output file, triggering regeneration,
47+
// which fires `watchChange` again.
48+
ignored: ["**/routeTree.gen.ts"],
49+
},
50+
},
3551
resolve: {
3652
tsconfigPaths: true,
37-
alias: {
38-
"use-sync-external-store/shim/with-selector.js": path.resolve(
39-
__dirname,
40-
"./src/polyfills/use-sync-external-store-shim.ts",
41-
),
42-
"use-sync-external-store/shim/with-selector": path.resolve(
43-
__dirname,
44-
"./src/polyfills/use-sync-external-store-shim.ts",
45-
),
46-
"use-sync-external-store/shim/index.js": path.resolve(
47-
__dirname,
48-
"./src/polyfills/use-sync-external-store-shim.ts",
49-
),
50-
"use-sync-external-store/shim": path.resolve(__dirname, "./src/polyfills/use-sync-external-store-shim.ts"),
51-
},
5253
},
5354
plugins: [
5455
tailwindcss(),
@@ -144,57 +145,50 @@ export default defineConfig({
144145
noExternal: ["better-auth"],
145146
},
146147
optimizeDeps: {
147-
exclude: ["better-auth"],
148-
// Pre-bundle the deps that vite would otherwise discover lazily on the
149-
// first request and then trigger a full client reload after pre-bundling
150-
// (the "✨ new dependencies optimized → ✨ reloading" pair in the dev
151-
// log). Without this list, the bump to @tanstack/react-start 1.167 +
152-
// better-auth 1.4 racks up ~38 newly-discovered deps on the first
153-
// request after `vite dev` starts, and the resulting mid-flight reload
154-
// breaks every Playwright test in that 10-15s window. The names below
155-
// are exactly what the dev-mode optimizer logged on a cold start —
156-
// keep them in sync if the dep tree shifts (the CI failure mode is
157-
// obvious: "✨ new dependencies optimized" lines you don't see in
158-
// the include list).
148+
// Pre-bundle deps Vite would otherwise discover at runtime. When the
149+
// dev server discovers a new dep mid-session it triggers a full
150+
// client reload after pre-bundling (the "✨ new dependencies
151+
// optimized → ✨ reloading" pair in the dev log), which invalidates
152+
// `react/jsx-runtime` hashes mid-flight and crashes any pages
153+
// hydrating in that 10-15s window. Listing the discoverable deps
154+
// here forces the first-request optimization to happen at startup
155+
// before Playwright is allowed to send its first navigation (the
156+
// readiness check on the dev server is HTTP, not just TCP).
157+
//
158+
// Mirrors gell-v2/vite.config.ts. If the dep tree shifts, watch for
159+
// "✨ new dependencies optimized" lines that aren't on this list
160+
// and add them.
159161
include: [
160-
"@better-auth/core/api",
161-
"@better-auth/core/context",
162-
"@better-auth/core/db",
163-
"@better-auth/core/db/adapter",
164-
"@better-auth/core/env",
165-
"@better-auth/core/error",
166-
"@better-auth/core/social-providers",
167-
"@better-auth/core/utils",
168-
"@better-auth/telemetry",
169-
"@better-auth/utils",
170-
"@better-auth/utils/base64",
171-
"@better-auth/utils/binary",
172-
"@better-auth/utils/hash",
173-
"@better-auth/utils/hex",
174-
"@better-auth/utils/hmac",
175-
"@better-auth/utils/random",
176-
"@better-fetch/fetch",
177-
"@noble/ciphers/chacha.js",
178-
"@noble/ciphers/utils.js",
179-
"@noble/hashes/hkdf.js",
180-
"@noble/hashes/scrypt.js",
181-
"@noble/hashes/sha2.js",
182-
"@noble/hashes/utils.js",
183162
"@tanstack/history",
184163
"@tanstack/router-core",
185-
"@tanstack/router-core/isServer",
186164
"@tanstack/router-core/ssr/client",
187165
"@tanstack/router-core/ssr/server",
188-
"better-call",
189-
"better-sse",
190166
"defu",
191-
"h3-v2",
192-
"jose",
193-
"jose/errors",
194-
"kysely",
195167
"nanostores",
196168
"seroval",
197-
"zod",
169+
"tiny-invariant",
170+
],
171+
exclude: [
172+
// better-auth ecosystem — server-only by design (drizzle / postgres
173+
// behind every export). Letting Vite pre-bundle these into the
174+
// client graph is the TSS-2 leak we sweep for in CI; keep them
175+
// out of the optimizer pass entirely.
176+
"better-auth",
177+
"@better-auth/core",
178+
"@better-auth/telemetry",
179+
"@better-auth/utils",
180+
"@better-fetch/fetch",
181+
"better-call",
182+
"better-sse",
183+
// better-auth transitive deps — no browser exports.
184+
"@noble/ciphers",
185+
"@noble/hashes",
186+
"jose",
187+
// Server-only framework deps leaked from SSR.
188+
"h3-v2",
198189
],
190+
// Force re-bundling under Playwright so stale-hash references from
191+
// a prior CI run can't break the first request.
192+
...(process.env.PLAYWRIGHT === "true" && { force: true }),
199193
},
200194
});

0 commit comments

Comments
 (0)