| author | |
| committer | |
| log | 80c708a1fa8986bce0b2d1ba4d35e79d9e4094ba |
| tree | dcca28606d6c84ef8f5d2920203129b67387388d |
| parent | a434677eca737771dd64703545c13eb11ba92ce2 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Theo Browne <me@t3.gg>9 files changed, 245 insertions(+), 44 deletions(-)
apps/web/src/components/AppSidebarLayout.tsx+13-2| ... | @@ -1,6 +1,8 @@ | ... | @@ -1,6 +1,8 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 3 | import { | 3 | import { |
| 4 | lazy, | ||
| 5 | Suspense, | ||
| 4 | useEffect, | 6 | useEffect, |
| 5 | useState, | 7 | useState, |
| 6 | useSyncExternalStore, | 8 | useSyncExternalStore, |
| ... | @@ -17,7 +19,6 @@ import { primaryServerKeybindingsAtom } from "../state/server"; | ... | @@ -17,7 +19,6 @@ import { primaryServerKeybindingsAtom } from "../state/server"; |
| 17 | import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings"; | 19 | import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings"; |
| 18 | import LegacyThreadSidebar from "./LegacySidebar"; | 20 | import LegacyThreadSidebar from "./LegacySidebar"; |
| 19 | import ThreadSidebar from "./Sidebar"; | 21 | import ThreadSidebar from "./Sidebar"; |
| 20 | import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; | ||
| 21 | import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; | 22 | import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| 22 | import { | 23 | import { |
| 23 | resolveSidebarStageFocusRingOffsetClass, | 24 | resolveSidebarStageFocusRingOffsetClass, |
| ... | @@ -43,6 +44,14 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | ... | @@ -43,6 +44,14 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 43 | 44 | ||
| 44 | const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; | 45 | const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; |
| 45 | 46 | ||
| 47 | // The settings nav (and the Clerk profile surfaces behind it) only renders on | ||
| 48 | // settings routes; lazy-loading it keeps that subtree out of the startup chunk. | ||
| 49 | const SettingsSidebarNav = lazy(() => | ||
| 50 | import("./settings/SettingsSidebarNav").then((module) => ({ | ||
| 51 | default: module.SettingsSidebarNav, | ||
| 52 | })), | ||
| 53 | ); | ||
| 54 | |||
| 46 | function subscribeToViewportWidth(onChange: () => void): () => void { | 55 | function subscribeToViewportWidth(onChange: () => void): () => void { |
| 47 | window.addEventListener("resize", onChange); | 56 | window.addEventListener("resize", onChange); |
| 48 | return () => window.removeEventListener("resize", onChange); | 57 | return () => window.removeEventListener("resize", onChange); |
| ... | @@ -229,7 +238,9 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -229,7 +238,9 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 229 | {isOnSettings ? ( | 238 | {isOnSettings ? ( |
| 230 | <> | 239 | <> |
| 231 | <SidebarChromeHeader isElectron={isElectron} /> | 240 | <SidebarChromeHeader isElectron={isElectron} /> |
| 232 | <SettingsSidebarNav pathname={pathname} /> | 241 | <Suspense fallback={null}> |
| 242 | <SettingsSidebarNav pathname={pathname} /> | ||
| 243 | </Suspense> | ||
| 233 | </> | 244 | </> |
| 234 | ) : legacySidebarEnabled ? ( | 245 | ) : legacySidebarEnabled ? ( |
| 235 | <LegacyThreadSidebar /> | 246 | <LegacyThreadSidebar /> |
apps/web/src/components/clerk/BrowserManagedAuthShell.tsx created+25| ... | @@ -0,0 +1,25 @@ | ||
| 1 | import { ClerkProvider } from "@clerk/react"; | ||
| 2 | import type { ReactNode } from "react"; | ||
| 3 | |||
| 4 | import { ManagedRelayAuthProvider } from "../../cloud/managedAuth"; | ||
| 5 | import { clerkAppearance } from "./clerkAppearance"; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Browser half of the managed-auth boundary, loaded lazily from the entry so | ||
| 9 | * cloudless local mode never downloads a Clerk runtime. The browser provider | ||
| 10 | * stays small on its own: it hotloads clerk-js at runtime instead of bundling | ||
| 11 | * it. | ||
| 12 | */ | ||
| 13 | export default function BrowserManagedAuthShell({ | ||
| 14 | publishableKey, | ||
| 15 | children, | ||
| 16 | }: { | ||
| 17 | readonly publishableKey: string; | ||
| 18 | readonly children: ReactNode; | ||
| 19 | }) { | ||
| 20 | return ( | ||
| 21 | <ClerkProvider appearance={clerkAppearance} publishableKey={publishableKey}> | ||
| 22 | <ManagedRelayAuthProvider>{children}</ManagedRelayAuthProvider> | ||
| 23 | </ClerkProvider> | ||
| 24 | ); | ||
| 25 | } | ||
apps/web/src/components/clerk/ElectronManagedAuthShell.tsx created+26| ... | @@ -0,0 +1,26 @@ | ||
| 1 | import { passkeys } from "@clerk/electron/passkeys"; | ||
| 2 | import { ClerkProvider } from "@clerk/electron/react"; | ||
| 3 | import type { ReactNode } from "react"; | ||
| 4 | |||
| 5 | import { ManagedRelayAuthProvider } from "../../cloud/managedAuth"; | ||
| 6 | import { clerkAppearance } from "./clerkAppearance"; | ||
| 7 | |||
| 8 | /** | ||
| 9 | * Electron half of the managed-auth boundary. The Electron provider statically | ||
| 10 | * bundles the full clerk-js runtime, so this module must only ever load | ||
| 11 | * lazily, and only inside the desktop shell — importing it eagerly would put | ||
| 12 | * clerk-js back into every client's startup graph. | ||
| 13 | */ | ||
| 14 | export default function ElectronManagedAuthShell({ | ||
| 15 | publishableKey, | ||
| 16 | children, | ||
| 17 | }: { | ||
| 18 | readonly publishableKey: string; | ||
| 19 | readonly children: ReactNode; | ||
| 20 | }) { | ||
| 21 | return ( | ||
| 22 | <ClerkProvider appearance={clerkAppearance} publishableKey={publishableKey} passkeys={passkeys}> | ||
| 23 | <ManagedRelayAuthProvider>{children}</ManagedRelayAuthProvider> | ||
| 24 | </ClerkProvider> | ||
| 25 | ); | ||
| 26 | } | ||
apps/web/src/components/settings/ThemeEditorHost.tsx+23-15| ... | @@ -1,11 +1,17 @@ | ... | @@ -1,11 +1,17 @@ |
| 1 | import { useCallback } from "react"; | 1 | import { lazy, Suspense, useCallback } from "react"; |
| 2 | 2 | ||
| 3 | import { useTheme } from "../../hooks/useTheme"; | 3 | import { useTheme } from "../../hooks/useTheme"; |
| 4 | import { getThemeDefinition, type ThemeAppearance, type ThemeDefinition } from "../../themePalette"; | 4 | import { getThemeDefinition, type ThemeAppearance, type ThemeDefinition } from "../../themePalette"; |
| 5 | import { stackedThreadToast, toastManager } from "../ui/toast"; | 5 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 6 | import { ThemeEditorPanel } from "./ThemeEditorPanel"; | ||
| 7 | import { useThemeEditorStore } from "./themeEditorStore"; | 6 | import { useThemeEditorStore } from "./themeEditorStore"; |
| 8 | 7 | ||
| 8 | // The host mounts above the router on every page, but the editor body only | ||
| 9 | // renders once a session opens; lazy-loading it keeps the editor UI out of | ||
| 10 | // the startup chunk. | ||
| 11 | const ThemeEditorPanel = lazy(() => | ||
| 12 | import("./ThemeEditorPanel").then((module) => ({ default: module.ThemeEditorPanel })), | ||
| 13 | ); | ||
| 14 | |||
| 9 | /** | 15 | /** |
| 10 | * Renders the theme editor above the router. The editor paints its draft on | 16 | * Renders the theme editor above the router. The editor paints its draft on |
| 11 | * the live app, so it has to outlive the settings route: the point is to walk | 17 | * the live app, so it has to outlive the settings route: the point is to walk |
| ... | @@ -97,18 +103,20 @@ export function ThemeEditorHost() { | ... | @@ -97,18 +103,20 @@ export function ThemeEditorHost() { |
| 97 | const seedTheme = session.seedThemeId ? (getThemeDefinition(session.seedThemeId) ?? null) : null; | 103 | const seedTheme = session.seedThemeId ? (getThemeDefinition(session.seedThemeId) ?? null) : null; |
| 98 | 104 | ||
| 99 | return ( | 105 | return ( |
| 100 | <ThemeEditorPanel | 106 | <Suspense fallback={null}> |
| 101 | editingTheme={editingTheme} | 107 | <ThemeEditorPanel |
| 102 | initialAppearance={session.initialAppearance} | 108 | editingTheme={editingTheme} |
| 103 | key={session.id} | 109 | initialAppearance={session.initialAppearance} |
| 104 | onOpenChange={(open) => { | 110 | key={session.id} |
| 105 | if (!open) closeThemeEditor(); | 111 | onOpenChange={(open) => { |
| 106 | }} | 112 | if (!open) closeThemeEditor(); |
| 107 | onSaved={handleSaved} | 113 | }} |
| 108 | open | 114 | onSaved={handleSaved} |
| 109 | restoreTheme={refreshTheme} | 115 | open |
| 110 | seedName={session.seedName ?? undefined} | 116 | restoreTheme={refreshTheme} |
| 111 | seedTheme={seedTheme} | 117 | seedName={session.seedName ?? undefined} |
| 112 | /> | 118 | seedTheme={seedTheme} |
| 119 | /> | ||
| 120 | </Suspense> | ||
| 113 | ); | 121 | ); |
| 114 | } | 122 | } |
apps/web/src/lib/chunkReloadGuard.test.ts created+56| ... | @@ -0,0 +1,56 @@ | ||
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | ||
| 2 | |||
| 3 | import { clearChunkReloadGuard, reloadOnceForChunkLoadError } from "./chunkReloadGuard"; | ||
| 4 | |||
| 5 | function createStorageStub(): Storage { | ||
| 6 | const store = new Map<string, string>(); | ||
| 7 | return { | ||
| 8 | getItem: (key) => store.get(key) ?? null, | ||
| 9 | setItem: (key, value) => { | ||
| 10 | store.set(key, value); | ||
| 11 | }, | ||
| 12 | removeItem: (key) => { | ||
| 13 | store.delete(key); | ||
| 14 | }, | ||
| 15 | clear: () => store.clear(), | ||
| 16 | key: (index) => [...store.keys()][index] ?? null, | ||
| 17 | get length() { | ||
| 18 | return store.size; | ||
| 19 | }, | ||
| 20 | }; | ||
| 21 | } | ||
| 22 | |||
| 23 | describe("reloadOnceForChunkLoadError", () => { | ||
| 24 | it("reloads on the first failure and lets the second one surface", () => { | ||
| 25 | const storage = createStorageStub(); | ||
| 26 | const reload = vi.fn(); | ||
| 27 | |||
| 28 | expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(true); | ||
| 29 | expect(reload).toHaveBeenCalledTimes(1); | ||
| 30 | |||
| 31 | expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(false); | ||
| 32 | expect(reload).toHaveBeenCalledTimes(1); | ||
| 33 | }); | ||
| 34 | |||
| 35 | it("reloads again after a successful boot cleared the guard", () => { | ||
| 36 | const storage = createStorageStub(); | ||
| 37 | const reload = vi.fn(); | ||
| 38 | |||
| 39 | reloadOnceForChunkLoadError(() => storage, reload); | ||
| 40 | clearChunkReloadGuard(() => storage); | ||
| 41 | |||
| 42 | expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(true); | ||
| 43 | expect(reload).toHaveBeenCalledTimes(2); | ||
| 44 | }); | ||
| 45 | |||
| 46 | it("never reloads when storage is blocked, so a persistent failure cannot loop", () => { | ||
| 47 | const reload = vi.fn(); | ||
| 48 | const blocked = () => { | ||
| 49 | throw new DOMException("blocked", "SecurityError"); | ||
| 50 | }; | ||
| 51 | |||
| 52 | expect(reloadOnceForChunkLoadError(blocked, reload)).toBe(false); | ||
| 53 | expect(reload).not.toHaveBeenCalled(); | ||
| 54 | expect(() => clearChunkReloadGuard(blocked)).not.toThrow(); | ||
| 55 | }); | ||
| 56 | }); | ||
apps/web/src/lib/chunkReloadGuard.ts created+39| ... | @@ -0,0 +1,39 @@ | ||
| 1 | // Split chunks are fetched lazily, so a deploy (or desktop server swap) | ||
| 2 | // between page load and a later fetch can 404 the old hashed assets. One | ||
| 3 | // reload picks up the fresh index.html. A sessionStorage flag keeps a | ||
| 4 | // persistent failure from becoming a reload loop, and a successful boot clears | ||
| 5 | // it so the next stale deploy gets its own single reload. | ||
| 6 | const CHUNK_RELOAD_GUARD_KEY = "t3code:chunk-load-reloaded"; | ||
| 7 | |||
| 8 | /** | ||
| 9 | * Called from the `vite:preloadError` listener. Reloads at most once per | ||
| 10 | * failure streak and returns whether it did, so the caller knows whether to | ||
| 11 | * swallow the event or let the error surface through the normal paths. | ||
| 12 | */ | ||
| 13 | export function reloadOnceForChunkLoadError( | ||
| 14 | getStorage: () => Storage = () => window.sessionStorage, | ||
| 15 | reload: () => void = () => window.location.reload(), | ||
| 16 | ): boolean { | ||
| 17 | let alreadyReloaded: boolean; | ||
| 18 | try { | ||
| 19 | const storage = getStorage(); | ||
| 20 | alreadyReloaded = storage.getItem(CHUNK_RELOAD_GUARD_KEY) === "1"; | ||
| 21 | if (!alreadyReloaded) storage.setItem(CHUNK_RELOAD_GUARD_KEY, "1"); | ||
| 22 | } catch { | ||
| 23 | // Without storage the guard cannot survive a reload, so a persistent | ||
| 24 | // failure would loop forever. Let the error surface instead. | ||
| 25 | return false; | ||
| 26 | } | ||
| 27 | if (alreadyReloaded) return false; | ||
| 28 | reload(); | ||
| 29 | return true; | ||
| 30 | } | ||
| 31 | |||
| 32 | /** Clears the guard after a successful boot so a later stale deploy can reload again. */ | ||
| 33 | export function clearChunkReloadGuard(getStorage: () => Storage = () => window.sessionStorage) { | ||
| 34 | try { | ||
| 35 | getStorage().removeItem(CHUNK_RELOAD_GUARD_KEY); | ||
| 36 | } catch { | ||
| 37 | // Blocked storage never held the flag. | ||
| 38 | } | ||
| 39 | } | ||
apps/web/src/main.tsx+55-26| ... | @@ -1,14 +1,10 @@ | ... | @@ -1,14 +1,10 @@ |
| 1 | import React from "react"; | 1 | import React from "react"; |
| 2 | import ReactDOM from "react-dom/client"; | 2 | import ReactDOM from "react-dom/client"; |
| 3 | import { ClerkProvider } from "@clerk/react"; | ||
| 4 | import { passkeys } from "@clerk/electron/passkeys"; | ||
| 5 | import { ClerkProvider as ElectronClerkProvider } from "@clerk/electron/react"; | ||
| 6 | import { createHashHistory, createBrowserHistory } from "@tanstack/react-router"; | 3 | import { createHashHistory, createBrowserHistory } from "@tanstack/react-router"; |
| 7 | 4 | ||
| 8 | import "./index.css"; | 5 | import "./index.css"; |
| 9 | 6 | ||
| 10 | import { isElectron } from "./env"; | 7 | import { isElectron } from "./env"; |
| 11 | import { ManagedRelayAuthProvider } from "./cloud/managedAuth"; | ||
| 12 | import { hasCloudPublicConfig } from "./cloud/publicConfig"; | 8 | import { hasCloudPublicConfig } from "./cloud/publicConfig"; |
| 13 | import { getRouter } from "./router"; | 9 | import { getRouter } from "./router"; |
| 14 | import { | 10 | import { |
| ... | @@ -16,7 +12,7 @@ import { | ... | @@ -16,7 +12,7 @@ import { |
| 16 | syncDocumentWindowControlsOverlayClass, | 12 | syncDocumentWindowControlsOverlayClass, |
| 17 | } from "./lib/windowControlsOverlay"; | 13 | } from "./lib/windowControlsOverlay"; |
| 18 | import { AppRoot } from "./AppRoot"; | 14 | import { AppRoot } from "./AppRoot"; |
| 19 | import { clerkAppearance } from "./components/clerk/clerkAppearance"; | 15 | import { clearChunkReloadGuard, reloadOnceForChunkLoadError } from "./lib/chunkReloadGuard"; |
| 20 | 16 | ||
| 21 | // Electron loads the app from a file-backed shell, so hash history avoids path resolution issues. | 17 | // Electron loads the app from a file-backed shell, so hash history avoids path resolution issues. |
| 22 | const history = isElectron ? createHashHistory() : createBrowserHistory(); | 18 | const history = isElectron ? createHashHistory() : createBrowserHistory(); |
| ... | @@ -30,26 +26,59 @@ if (isElectron) { | ... | @@ -30,26 +26,59 @@ if (isElectron) { |
| 30 | 26 | ||
| 31 | const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; | 27 | const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; |
| 32 | 28 | ||
| 29 | // A failed split-chunk fetch usually means the hashed assets went stale under | ||
| 30 | // a deploy; one guarded reload picks up the fresh index.html. | ||
| 31 | let chunkLoadFailed = false; | ||
| 32 | let reloadScheduled = false; | ||
| 33 | window.addEventListener("vite:preloadError", (event) => { | ||
| 34 | chunkLoadFailed = true; | ||
| 35 | if (reloadOnceForChunkLoadError()) { | ||
| 36 | reloadScheduled = true; | ||
| 37 | event.preventDefault(); | ||
| 38 | } | ||
| 39 | }); | ||
| 40 | |||
| 33 | const app = <AppRoot router={router} />; | 41 | const app = <AppRoot router={router} />; |
| 34 | 42 | ||
| 35 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( | 43 | // Managed auth is cloud-only, and the Electron Clerk provider bundles the full |
| 36 | <React.StrictMode> | 44 | // clerk-js runtime. Loading only the selected runtime as a split chunk keeps |
| 37 | {clerkPublishableKey && hasCloudPublicConfig() ? ( | 45 | // every Clerk byte out of the startup graph for local-mode users, and keeps |
| 38 | isElectron ? ( | 46 | // the bundled clerk-js out of the browser build entirely. |
| 39 | <ElectronClerkProvider | 47 | const managedAuthShellModule = |
| 40 | appearance={clerkAppearance} | 48 | clerkPublishableKey && hasCloudPublicConfig() |
| 41 | publishableKey={clerkPublishableKey} | 49 | ? isElectron |
| 42 | passkeys={passkeys} | 50 | ? import("./components/clerk/ElectronManagedAuthShell") |
| 43 | > | 51 | : import("./components/clerk/BrowserManagedAuthShell") |
| 44 | <ManagedRelayAuthProvider>{app}</ManagedRelayAuthProvider> | 52 | : null; |
| 45 | </ElectronClerkProvider> | 53 | |
| 46 | ) : ( | 54 | // The index.html boot splash lives inside #root, and React's first commit |
| 47 | <ClerkProvider appearance={clerkAppearance} publishableKey={clerkPublishableKey}> | 55 | // clears it. Resolve everything that first commit needs, the selected |
| 48 | <ManagedRelayAuthProvider>{app}</ManagedRelayAuthProvider> | 56 | // managed-auth runtime and the initial route's split chunks, before |
| 49 | </ClerkProvider> | 57 | // rendering, so the splash holds until real UI paints instead of dropping to |
| 50 | ) | 58 | // a blank window while chunks download. |
| 51 | ) : ( | 59 | void Promise.all([managedAuthShellModule?.then((module) => module.default) ?? null, router.load()]) |
| 52 | app | 60 | .then(([ManagedAuthShell]) => { |
| 53 | )} | 61 | // A route chunk failure still resolves router.load(): the error is parked in |
| 54 | </React.StrictMode>, | 62 | // the lazy component and surfaces through the route error boundary. Skip the |
| 55 | ); | 63 | // paint when a reload is on its way, and only re-arm the guard after a boot |
| 64 | // that fetched every chunk it asked for. | ||
| 65 | if (reloadScheduled) return; | ||
| 66 | if (!chunkLoadFailed) clearChunkReloadGuard(); | ||
| 67 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( | ||
| 68 | <React.StrictMode> | ||
| 69 | {ManagedAuthShell && clerkPublishableKey ? ( | ||
| 70 | <ManagedAuthShell publishableKey={clerkPublishableKey}>{app}</ManagedAuthShell> | ||
| 71 | ) : ( | ||
| 72 | app | ||
| 73 | )} | ||
| 74 | </React.StrictMode>, | ||
| 75 | ); | ||
| 76 | }) | ||
| 77 | .catch((error: unknown) => { | ||
| 78 | // The auth shell chunk failed and the guarded reload is spent. Say so | ||
| 79 | // instead of leaving the splash up forever. | ||
| 80 | if (reloadScheduled) return; | ||
| 81 | console.error("T3 Code failed to load its startup chunks.", error); | ||
| 82 | const bootShell = document.getElementById("boot-shell"); | ||
| 83 | if (bootShell) bootShell.textContent = "T3 Code could not load. Reload to try again."; | ||
| 84 | }); |
apps/web/src/router.ts+4| ... | @@ -7,6 +7,10 @@ export function getRouter(history: RouterHistory) { | ... | @@ -7,6 +7,10 @@ export function getRouter(history: RouterHistory) { |
| 7 | routeTree, | 7 | routeTree, |
| 8 | history, | 8 | history, |
| 9 | context: {}, | 9 | context: {}, |
| 10 | // Route components are split chunks (autoCodeSplitting in vite.config); | ||
| 11 | // fetching them on hover/focus intent hides the load from the first | ||
| 12 | // settings or pull-request navigation. | ||
| 13 | defaultPreload: "intent", | ||
| 10 | }); | 14 | }); |
| 11 | } | 15 | } |
| 12 | 16 |
apps/web/vite.config.ts+4-1| ... | @@ -156,7 +156,10 @@ export default defineConfig(() => { | ... | @@ -156,7 +156,10 @@ export default defineConfig(() => { |
| 156 | assetsInclude: ["**/*.wasm"], | 156 | assetsInclude: ["**/*.wasm"], |
| 157 | plugins: [ | 157 | plugins: [ |
| 158 | devCompressionPlugin(), | 158 | devCompressionPlugin(), |
| 159 | tanstackRouter(), | 159 | // Route components load as split chunks so settings, pull-request, and |
| 160 | // usage code stay out of the cold-start payload; the router prefetches | ||
| 161 | // them on navigation intent (see getRouter's defaultPreload). | ||
| 162 | tanstackRouter({ autoCodeSplitting: true }), | ||
| 160 | react(), | 163 | react(), |
| 161 | babel({ | 164 | babel({ |
| 162 | // We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0 | 165 | // We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0 |