diff --git a/e2e/react-start/server-functions/src/routes/factory/-functions/functions.ts b/e2e/react-start/server-functions/src/routes/factory/-functions/functions.ts
index 35be5f91f7..ef71c4f6c2 100644
--- a/e2e/react-start/server-functions/src/routes/factory/-functions/functions.ts
+++ b/e2e/react-start/server-functions/src/routes/factory/-functions/functions.ts
@@ -76,7 +76,7 @@ export const localFnPOST = localFnFactory({ method: 'POST' })
 export const fakeFn = createFakeFn().handler(async () => {
   return {
     name: 'fakeFn',
-    window,
+    window: typeof window !== 'undefined' ? window : 'no window object',
   }
 })
 
diff --git a/e2e/react-start/server-functions/src/routes/factory/index.tsx b/e2e/react-start/server-functions/src/routes/factory/index.tsx
index 2186aae9ff..582e2ab4b8 100644
--- a/e2e/react-start/server-functions/src/routes/factory/index.tsx
+++ b/e2e/react-start/server-functions/src/routes/factory/index.tsx
@@ -127,7 +127,7 @@ const functions = {
     type: 'localFn',
     expected: {
       name: 'fakeFn',
-      window,
+      window: typeof window !== 'undefined' ? window : 'no window object',
     },
   },
 } satisfies Record<string, TestCase>
diff --git a/e2e/react-start/split-exports/.gitignore b/e2e/react-start/split-exports/.gitignore
new file mode 100644
index 0000000000..1cd91e546b
--- /dev/null
+++ b/e2e/react-start/split-exports/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+dist
+.output
+.vite
+*.local
diff --git a/e2e/react-start/split-exports/.prettierignore b/e2e/react-start/split-exports/.prettierignore
new file mode 100644
index 0000000000..378756846d
--- /dev/null
+++ b/e2e/react-start/split-exports/.prettierignore
@@ -0,0 +1,2 @@
+pnpm-lock.yaml
+routeTree.gen.ts
diff --git a/e2e/react-start/split-exports/package.json b/e2e/react-start/split-exports/package.json
new file mode 100644
index 0000000000..3f36351a4e
--- /dev/null
+++ b/e2e/react-start/split-exports/package.json
@@ -0,0 +1,38 @@
+{
+  "name": "tanstack-react-start-e2e-split-exports",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port ${PORT:-3000}",
+    "dev:e2e": "vite dev",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e": "rm -rf port*.txt; playwright test --project=chromium",
+    "test:e2e:hmr": "rm -rf port*.txt; playwright test --config=playwright-hmr.config.ts"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/react-router-devtools": "workspace:^",
+    "@tanstack/react-start": "workspace:^",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+    "tailwind-merge": "^2.6.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tailwindcss/postcss": "^4.1.15",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^4.3.4",
+    "postcss": "^8.5.1",
+    "srvx": "^0.8.6",
+    "tailwindcss": "^4.1.17",
+    "typescript": "^5.7.2",
+    "vite": "^7.1.7",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/react-start/split-exports/playwright-hmr.config.ts b/e2e/react-start/split-exports/playwright-hmr.config.ts
new file mode 100644
index 0000000000..5c8df2a0f1
--- /dev/null
+++ b/e2e/react-start/split-exports/playwright-hmr.config.ts
@@ -0,0 +1,59 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+/**
+ * Playwright configuration for HMR tests.
+ *
+ * This config runs the dev server instead of a production build,
+ * allowing tests to verify Hot Module Replacement works correctly
+ * with the split-exports plugin.
+ *
+ * Run with: npx playwright test --config=playwright-hmr.config.ts
+ */
+
+// Use a different port for HMR tests to avoid conflicts
+const PORT = (await getTestServerPort(packageJson.name)) + 1
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  // Only run HMR test files
+  testMatch: '*-hmr.spec.ts',
+  // HMR tests can be slower due to file system operations
+  timeout: 60000,
+  // Run serially to avoid file system conflicts
+  workers: 1,
+  reporter: [['line']],
+
+  use: {
+    baseURL,
+    // Longer timeouts for HMR operations
+    actionTimeout: 10000,
+    navigationTimeout: 30000,
+  },
+
+  webServer: {
+    // Use dev server instead of build
+    command: `pnpm dev`,
+    url: baseURL,
+    // Always start fresh for HMR tests
+    reuseExistingServer: false,
+    stdout: 'pipe',
+    stderr: 'pipe',
+    // Dev server may take longer to start
+    timeout: 60000,
+    env: {
+      PORT: String(PORT),
+    },
+  },
+
+  projects: [
+    {
+      name: 'chromium-hmr',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/react-start/split-exports/playwright.config.ts b/e2e/react-start/split-exports/playwright.config.ts
new file mode 100644
index 0000000000..aa75d2d350
--- /dev/null
+++ b/e2e/react-start/split-exports/playwright.config.ts
@@ -0,0 +1,37 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  // Exclude HMR tests - they require dev server (use playwright-hmr.config.ts)
+  testIgnore: '*-hmr.spec.ts',
+  workers: 1,
+  reporter: [['line']],
+
+  use: {
+    baseURL,
+  },
+
+  webServer: {
+    command: `pnpm build && pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+    env: {
+      PORT: String(PORT),
+    },
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/react-start/split-exports/postcss.config.mjs b/e2e/react-start/split-exports/postcss.config.mjs
new file mode 100644
index 0000000000..a7f73a2d1d
--- /dev/null
+++ b/e2e/react-start/split-exports/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+  plugins: {
+    '@tailwindcss/postcss': {},
+  },
+}
diff --git a/e2e/react-start/split-exports/src/routeTree.gen.ts b/e2e/react-start/split-exports/src/routeTree.gen.ts
new file mode 100644
index 0000000000..45c0a7f3e4
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routeTree.gen.ts
@@ -0,0 +1,156 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ServerRequestImportRouteImport } from './routes/server-request-import'
+import { Route as ReexportImportRouteImport } from './routes/reexport-import'
+import { Route as DirectImportRouteImport } from './routes/direct-import'
+import { Route as AliasImportRouteImport } from './routes/alias-import'
+import { Route as IndexRouteImport } from './routes/index'
+
+const ServerRequestImportRoute = ServerRequestImportRouteImport.update({
+  id: '/server-request-import',
+  path: '/server-request-import',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const ReexportImportRoute = ReexportImportRouteImport.update({
+  id: '/reexport-import',
+  path: '/reexport-import',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const DirectImportRoute = DirectImportRouteImport.update({
+  id: '/direct-import',
+  path: '/direct-import',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const AliasImportRoute = AliasImportRouteImport.update({
+  id: '/alias-import',
+  path: '/alias-import',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '/alias-import': typeof AliasImportRoute
+  '/direct-import': typeof DirectImportRoute
+  '/reexport-import': typeof ReexportImportRoute
+  '/server-request-import': typeof ServerRequestImportRoute
+}
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '/alias-import': typeof AliasImportRoute
+  '/direct-import': typeof DirectImportRoute
+  '/reexport-import': typeof ReexportImportRoute
+  '/server-request-import': typeof ServerRequestImportRoute
+}
+export interface FileRoutesById {
+  __root__: typeof rootRouteImport
+  '/': typeof IndexRoute
+  '/alias-import': typeof AliasImportRoute
+  '/direct-import': typeof DirectImportRoute
+  '/reexport-import': typeof ReexportImportRoute
+  '/server-request-import': typeof ServerRequestImportRoute
+}
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths:
+    | '/'
+    | '/alias-import'
+    | '/direct-import'
+    | '/reexport-import'
+    | '/server-request-import'
+  fileRoutesByTo: FileRoutesByTo
+  to:
+    | '/'
+    | '/alias-import'
+    | '/direct-import'
+    | '/reexport-import'
+    | '/server-request-import'
+  id:
+    | '__root__'
+    | '/'
+    | '/alias-import'
+    | '/direct-import'
+    | '/reexport-import'
+    | '/server-request-import'
+  fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  AliasImportRoute: typeof AliasImportRoute
+  DirectImportRoute: typeof DirectImportRoute
+  ReexportImportRoute: typeof ReexportImportRoute
+  ServerRequestImportRoute: typeof ServerRequestImportRoute
+}
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/server-request-import': {
+      id: '/server-request-import'
+      path: '/server-request-import'
+      fullPath: '/server-request-import'
+      preLoaderRoute: typeof ServerRequestImportRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/reexport-import': {
+      id: '/reexport-import'
+      path: '/reexport-import'
+      fullPath: '/reexport-import'
+      preLoaderRoute: typeof ReexportImportRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/direct-import': {
+      id: '/direct-import'
+      path: '/direct-import'
+      fullPath: '/direct-import'
+      preLoaderRoute: typeof DirectImportRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/alias-import': {
+      id: '/alias-import'
+      path: '/alias-import'
+      fullPath: '/alias-import'
+      preLoaderRoute: typeof AliasImportRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+  }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  AliasImportRoute: AliasImportRoute,
+  DirectImportRoute: DirectImportRoute,
+  ReexportImportRoute: ReexportImportRoute,
+  ServerRequestImportRoute: ServerRequestImportRoute,
+}
+export const routeTree = rootRouteImport
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+  interface Register {
+    ssr: true
+    router: Awaited<ReturnType<typeof getRouter>>
+  }
+}
diff --git a/e2e/react-start/split-exports/src/router.tsx b/e2e/react-start/split-exports/src/router.tsx
new file mode 100644
index 0000000000..16fd65460d
--- /dev/null
+++ b/e2e/react-start/split-exports/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: 'intent',
+  })
+
+  return router
+}
diff --git a/e2e/react-start/split-exports/src/routes/__root.tsx b/e2e/react-start/split-exports/src/routes/__root.tsx
new file mode 100644
index 0000000000..81aff73ef0
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+/// <reference types="vite/client" />
+import * as React from 'react'
+import {
+  HeadContent,
+  Link,
+  Outlet,
+  Scripts,
+  createRootRoute,
+} from '@tanstack/react-router'
+import appCss from '~/styles/app.css?url'
+
+export const Route = createRootRoute({
+  head: () => ({
+    meta: [
+      {
+        charSet: 'utf-8',
+      },
+      {
+        name: 'viewport',
+        content: 'width=device-width, initial-scale=1',
+      },
+      {
+        title: 'Split Exports E2E Test',
+      },
+    ],
+    links: [{ rel: 'stylesheet', href: appCss }],
+  }),
+  component: RootComponent,
+})
+
+function RootComponent() {
+  return (
+    <html>
+      <head>
+        <HeadContent />
+      </head>
+      <body>
+        <div className="p-2 flex gap-2 text-lg">
+          <Link
+            to="/"
+            activeProps={{ className: 'font-bold' }}
+            activeOptions={{ exact: true }}
+          >
+            Home
+          </Link>
+          <Link to="/direct-import" activeProps={{ className: 'font-bold' }}>
+            Direct Import
+          </Link>
+          <Link to="/reexport-import" activeProps={{ className: 'font-bold' }}>
+            Re-export Import
+          </Link>
+          <Link to="/alias-import" activeProps={{ className: 'font-bold' }}>
+            Alias Import
+          </Link>
+        </div>
+        <hr />
+        <Outlet />
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/routes/alias-import.tsx b/e2e/react-start/split-exports/src/routes/alias-import.tsx
new file mode 100644
index 0000000000..12bfbc6952
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/alias-import.tsx
@@ -0,0 +1,171 @@
+/**
+ * Test: Import using TypeScript path aliases.
+ *
+ * This route uses the ~/ alias to import modules.
+ * Tests that the split-exports plugin correctly handles aliased imports.
+ *
+ * This test also covers the nested import scenario - importing from nested.ts
+ * which internally uses server-only code but only exposes isomorphic functions.
+ */
+import { createFileRoute } from '@tanstack/react-router'
+import { useState } from 'react'
+
+// Import using path alias from shared.ts
+import {
+  getEnvironment,
+  getServerEnvironment,
+  getUserById,
+  formatUserName,
+  APP_NAME,
+} from '~/utils/shared'
+
+// Import from nested module using path alias
+// This tests the nested import scenario where nested.ts internally uses
+// server-only code (getServerOnlyUserData) but only exposes isomorphic functions
+import {
+  fetchUserProfile,
+  computeGreeting,
+  getServerGreeting,
+  GREETING_PREFIX,
+} from '~/utils/nested'
+
+export const Route = createFileRoute('/alias-import')({
+  component: AliasImportTest,
+  loader: async () => {
+    const envOnLoad = getEnvironment()
+    const greetingOnLoad = computeGreeting('SSR via Alias')
+    const user = await getUserById({ data: '100' })
+    // Test fetchUserProfile from nested.ts (uses server-only code internally)
+    const profile = await fetchUserProfile({ data: '42' })
+    return { envOnLoad, greetingOnLoad, user, profile }
+  },
+})
+
+function AliasImportTest() {
+  const { envOnLoad, greetingOnLoad, user, profile } = Route.useLoaderData()
+  const [results, setResults] = useState<{
+    envOnClick?: string
+    greetingOnClick?: string
+    serverEnv?: string
+    serverGreeting?: string
+    serverProfile?: {
+      id: string
+      displayName: string
+      contact: string
+      appName: string
+    }
+  } | null>(null)
+
+  async function handleClick() {
+    const envOnClick = getEnvironment()
+    const greetingOnClick = computeGreeting('Client via Alias')
+    const [serverEnv, serverGreeting, serverProfile] = await Promise.all([
+      getServerEnvironment(),
+      getServerGreeting({ data: 'Server via Alias' }),
+      fetchUserProfile({ data: '99' }),
+    ])
+    setResults({
+      envOnClick,
+      greetingOnClick,
+      serverEnv,
+      serverGreeting,
+      serverProfile,
+    })
+  }
+
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-4">Alias Import Test</h1>
+      <p className="mb-4">
+        Testing imports using TypeScript path aliases (~/utils/...). Also tests
+        nested imports where the module internally uses server-only code.
+      </p>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">
+          Constants (from aliased imports):
+        </h2>
+        <div>
+          App Name: <pre data-testid="app-name">{APP_NAME}</pre>
+        </div>
+        <div>
+          Greeting Prefix:{' '}
+          <pre data-testid="greeting-prefix">{GREETING_PREFIX}</pre>
+        </div>
+      </div>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">
+          Format User Name (pure function):
+        </h2>
+        <pre data-testid="formatted-name">
+          {formatUserName('Jane', 'Smith')}
+        </pre>
+      </div>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">SSR Results:</h2>
+        <div>
+          Environment on load:
+          <pre data-testid="ssr-env">{JSON.stringify(envOnLoad)}</pre>
+        </div>
+        <div>
+          Greeting on load:
+          <pre data-testid="ssr-greeting">{JSON.stringify(greetingOnLoad)}</pre>
+        </div>
+        <div>
+          User from server:
+          <pre data-testid="ssr-user">{JSON.stringify(user)}</pre>
+        </div>
+        <div>
+          Profile from nested module (uses server-only code internally):
+          <pre data-testid="ssr-profile">{JSON.stringify(profile)}</pre>
+        </div>
+      </div>
+
+      <button
+        onClick={handleClick}
+        className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
+        data-testid="run-client-tests-btn"
+      >
+        Run Client Tests
+      </button>
+
+      {results && (
+        <div className="space-y-4">
+          <h2 className="text-lg font-semibold">Client Results:</h2>
+          <div>
+            Environment on click:
+            <pre data-testid="client-env">
+              {JSON.stringify(results.envOnClick)}
+            </pre>
+          </div>
+          <div>
+            Greeting on click:
+            <pre data-testid="client-greeting">
+              {JSON.stringify(results.greetingOnClick)}
+            </pre>
+          </div>
+          <div>
+            Server environment:
+            <pre data-testid="server-env">
+              {JSON.stringify(results.serverEnv)}
+            </pre>
+          </div>
+          <div>
+            Server greeting:
+            <pre data-testid="server-greeting">
+              {JSON.stringify(results.serverGreeting)}
+            </pre>
+          </div>
+          <div>
+            Server profile (from nested module):
+            <pre data-testid="server-profile">
+              {JSON.stringify(results.serverProfile)}
+            </pre>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/routes/direct-import.tsx b/e2e/react-start/split-exports/src/routes/direct-import.tsx
new file mode 100644
index 0000000000..30407e732d
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/direct-import.tsx
@@ -0,0 +1,126 @@
+/**
+ * Test: Direct import from a module with mixed exports.
+ *
+ * This route imports isomorphic functions directly from shared.ts,
+ * which also exports server-only code that should NOT be bundled.
+ */
+import { createFileRoute } from '@tanstack/react-router'
+import { useState } from 'react'
+
+// Direct import - only importing the isomorphic exports
+// Server-only exports (getServerOnlyDatabase, getServerOnlyUserData, serverOnlyConfig)
+// should be eliminated from the client bundle by the split-exports plugin
+import {
+  getEnvironment,
+  getServerEnvironment,
+  formatMessage,
+  getUserById,
+  formatUserName,
+  APP_NAME,
+} from '../utils/shared'
+
+export const Route = createFileRoute('/direct-import')({
+  component: DirectImportTest,
+  loader: async () => {
+    // Call isomorphic function during SSR - should return 'server'
+    const envOnLoad = getEnvironment()
+    // Call server function during SSR
+    const user = await getUserById({ data: '1' })
+    return { envOnLoad, user }
+  },
+})
+
+function DirectImportTest() {
+  const { envOnLoad, user } = Route.useLoaderData()
+  const [results, setResults] = useState<{
+    envOnClick?: string
+    messageOnClick?: string
+    serverEnv?: string
+    serverUser?: { id: string; name: string; email: string }
+  } | null>(null)
+
+  async function handleClick() {
+    // Call isomorphic function on client - should return 'client'
+    const envOnClick = getEnvironment()
+    // Call isomorphic function on client
+    const messageOnClick = formatMessage('Hello World')
+    // Call server functions from client
+    const [serverEnv, serverUser] = await Promise.all([
+      getServerEnvironment(),
+      getUserById({ data: '2' }),
+    ])
+    setResults({ envOnClick, messageOnClick, serverEnv, serverUser })
+  }
+
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-4">Direct Import Test</h1>
+      <p className="mb-4">
+        Testing direct imports from a module with mixed server-only and
+        isomorphic exports.
+      </p>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">App Name (constant):</h2>
+        <pre data-testid="app-name">{APP_NAME}</pre>
+      </div>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">
+          Format User Name (pure function):
+        </h2>
+        <pre data-testid="formatted-name">{formatUserName('John', 'Doe')}</pre>
+      </div>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">SSR Results:</h2>
+        <div>
+          Environment on load (should be "server"):
+          <pre data-testid="ssr-env">{JSON.stringify(envOnLoad)}</pre>
+        </div>
+        <div>
+          User loaded during SSR:
+          <pre data-testid="ssr-user">{JSON.stringify(user)}</pre>
+        </div>
+      </div>
+
+      <button
+        onClick={handleClick}
+        className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
+        data-testid="run-client-tests-btn"
+      >
+        Run Client Tests
+      </button>
+
+      {results && (
+        <div className="space-y-4">
+          <h2 className="text-lg font-semibold">Client Results:</h2>
+          <div>
+            Environment on click (should be "client"):
+            <pre data-testid="client-env">
+              {JSON.stringify(results.envOnClick)}
+            </pre>
+          </div>
+          <div>
+            Message formatted on client:
+            <pre data-testid="client-message">
+              {JSON.stringify(results.messageOnClick)}
+            </pre>
+          </div>
+          <div>
+            Server environment (via server function):
+            <pre data-testid="server-env">
+              {JSON.stringify(results.serverEnv)}
+            </pre>
+          </div>
+          <div>
+            User from server function:
+            <pre data-testid="server-user">
+              {JSON.stringify(results.serverUser)}
+            </pre>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/routes/index.tsx b/e2e/react-start/split-exports/src/routes/index.tsx
new file mode 100644
index 0000000000..3055063034
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/index.tsx
@@ -0,0 +1,38 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+  component: Home,
+})
+
+function Home() {
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-4" data-testid="home-title">
+        Split Exports Plugin E2E Tests
+      </h1>
+      <p className="mb-4">
+        This e2e test verifies that the split-exports plugin correctly handles
+        modules that export both server-only and isomorphic code.
+      </p>
+      <h2 className="text-xl font-semibold mb-2">Test Scenarios:</h2>
+      <ul className="list-disc pl-6 space-y-2">
+        <li>
+          <strong>Direct Import:</strong> Import isomorphic functions directly
+          from a module that also exports server-only code
+        </li>
+        <li>
+          <strong>Re-export Import:</strong> Import from a module that
+          re-exports isomorphic functions from another module
+        </li>
+        <li>
+          <strong>Nested Import:</strong> Import from a module that internally
+          uses server-only code but only exposes isomorphic functions
+        </li>
+        <li>
+          <strong>Alias Import:</strong> Import using TypeScript path aliases
+          (~/utils/...)
+        </li>
+      </ul>
+    </div>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/routes/reexport-import.tsx b/e2e/react-start/split-exports/src/routes/reexport-import.tsx
new file mode 100644
index 0000000000..4b2a770243
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/reexport-import.tsx
@@ -0,0 +1,100 @@
+/**
+ * Test: Import from a module that re-exports.
+ *
+ * This route imports from public-api.ts, which re-exports
+ * isomorphic functions from shared.ts.
+ */
+import { createFileRoute } from '@tanstack/react-router'
+import { useState } from 'react'
+
+// Import from the re-export module
+import {
+  getEnv, // renamed re-export
+  getServerEnvironment,
+  formatMessage,
+  getAllUsers,
+  APP_NAME,
+} from '../utils/public-api'
+
+export const Route = createFileRoute('/reexport-import')({
+  component: ReexportImportTest,
+  loader: async () => {
+    const envOnLoad = getEnv()
+    const users = await getAllUsers()
+    return { envOnLoad, users }
+  },
+})
+
+function ReexportImportTest() {
+  const { envOnLoad, users } = Route.useLoaderData()
+  const [results, setResults] = useState<{
+    envOnClick?: string
+    messageOnClick?: string
+    serverEnv?: string
+  } | null>(null)
+
+  async function handleClick() {
+    const envOnClick = getEnv()
+    const messageOnClick = formatMessage('Re-export test')
+    const serverEnv = await getServerEnvironment()
+    setResults({ envOnClick, messageOnClick, serverEnv })
+  }
+
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-4">Re-export Import Test</h1>
+      <p className="mb-4">
+        Testing imports from a module that re-exports isomorphic functions.
+      </p>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">App Name:</h2>
+        <pre data-testid="app-name">{APP_NAME}</pre>
+      </div>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold">SSR Results:</h2>
+        <div>
+          Environment on load:
+          <pre data-testid="ssr-env">{JSON.stringify(envOnLoad)}</pre>
+        </div>
+        <div>
+          All users from server:
+          <pre data-testid="ssr-users">{JSON.stringify(users)}</pre>
+        </div>
+      </div>
+
+      <button
+        onClick={handleClick}
+        className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
+        data-testid="run-client-tests-btn"
+      >
+        Run Client Tests
+      </button>
+
+      {results && (
+        <div className="space-y-4">
+          <h2 className="text-lg font-semibold">Client Results:</h2>
+          <div>
+            Environment on click:
+            <pre data-testid="client-env">
+              {JSON.stringify(results.envOnClick)}
+            </pre>
+          </div>
+          <div>
+            Message formatted:
+            <pre data-testid="client-message">
+              {JSON.stringify(results.messageOnClick)}
+            </pre>
+          </div>
+          <div>
+            Server environment:
+            <pre data-testid="server-env">
+              {JSON.stringify(results.serverEnv)}
+            </pre>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/routes/server-request-import.tsx b/e2e/react-start/split-exports/src/routes/server-request-import.tsx
new file mode 100644
index 0000000000..fe5feeaecd
--- /dev/null
+++ b/e2e/react-start/split-exports/src/routes/server-request-import.tsx
@@ -0,0 +1,146 @@
+/**
+ * Test: Nested server-only imports through createServerFn
+ *
+ * This route tests the following chain:
+ *
+ * 1. This route imports `getRequestInfoServerFn` (a createServerFn) from
+ *    '../utils/server-request'
+ *
+ * 2. Inside server-request.ts, the createServerFn handler calls an internal
+ *    function `getRequestInfo()` which uses:
+ *    `import { getRequest } from '@tanstack/react-start/server'`
+ *
+ * 3. `getRequest` is a server-only API that cannot run on the client
+ *
+ * The split-exports plugin must ensure that when this route only imports
+ * the isomorphic `getRequestInfoServerFn`, the server-only `getRequest`
+ * import (and any other server-only exports) are eliminated from the
+ * client bundle through dead code elimination.
+ *
+ * This validates that import rewriting works correctly through multiple
+ * levels of the module graph.
+ */
+import { createFileRoute } from '@tanstack/react-router'
+import { useState } from 'react'
+
+// Import only the isomorphic server function - the server-only exports
+// (getServerOnlyRequestHeaders, SERVER_REQUEST_MARKER) should be eliminated
+import {
+  getRequestInfoServerFn,
+  echoWithRequestInfo,
+} from '../utils/server-request'
+
+export const Route = createFileRoute('/server-request-import')({
+  component: ServerRequestImportTest,
+  loader: async () => {
+    // Call the server function as a loader
+    // This should return method: "GET" because loaders use GET requests
+    const requestInfo = await getRequestInfoServerFn()
+    return { requestInfo }
+  },
+})
+
+function ServerRequestImportTest() {
+  const { requestInfo } = Route.useLoaderData()
+  const [clientResults, setClientResults] = useState<{
+    requestInfo?: { method: string; pathname: string; executedOn: string }
+    echoResult?: { echo: string; method: string; executedOn: string }
+  } | null>(null)
+
+  async function handleClick() {
+    // Call server functions from client
+    // These will be POST requests since they're called from client-side code
+    const [requestInfo, echoResult] = await Promise.all([
+      getRequestInfoServerFn(),
+      echoWithRequestInfo({ data: { message: 'Hello from client!' } }),
+    ])
+    setClientResults({ requestInfo, echoResult })
+  }
+
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-4">Server Request Import Test</h1>
+      <p className="mb-4">
+        Testing that imports inside files with ?tss-split-exports query are
+        properly rewritten. This verifies that server-only imports (like
+        getRequest from @tanstack/react-start/server) are eliminated from client
+        bundles.
+      </p>
+
+      <div className="mb-6 p-4 bg-gray-100 rounded">
+        <h2 className="text-lg font-semibold mb-2">SSR/Loader Results:</h2>
+        <div className="space-y-2">
+          <div>
+            <span className="font-medium">Request Method (should be GET):</span>
+            <pre data-testid="loader-method" className="inline ml-2">
+              {requestInfo.method}
+            </pre>
+          </div>
+          <div>
+            <span className="font-medium">Executed On:</span>
+            <pre data-testid="loader-executed-on" className="inline ml-2">
+              {requestInfo.executedOn}
+            </pre>
+          </div>
+          <div>
+            <span className="font-medium">Full Result:</span>
+            <pre data-testid="loader-result" className="block mt-1">
+              {JSON.stringify(requestInfo, null, 2)}
+            </pre>
+          </div>
+        </div>
+      </div>
+
+      <button
+        onClick={handleClick}
+        className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
+        data-testid="run-client-tests-btn"
+      >
+        Run Client Tests
+      </button>
+
+      {clientResults && (
+        <div className="p-4 bg-green-50 rounded">
+          <h2 className="text-lg font-semibold mb-2">Client Results:</h2>
+          <div className="space-y-4">
+            <div>
+              <h3 className="font-medium">getRequestInfoServerFn result:</h3>
+              <div className="ml-4 space-y-1">
+                <div>
+                  <span>Method (should be GET):</span>
+                  <pre data-testid="client-method" className="inline ml-2">
+                    {clientResults.requestInfo?.method}
+                  </pre>
+                </div>
+                <div>
+                  <span>Executed On:</span>
+                  <pre data-testid="client-executed-on" className="inline ml-2">
+                    {clientResults.requestInfo?.executedOn}
+                  </pre>
+                </div>
+              </div>
+            </div>
+
+            <div>
+              <h3 className="font-medium">echoWithRequestInfo result:</h3>
+              <div className="ml-4 space-y-1">
+                <div>
+                  <span>Echo:</span>
+                  <pre data-testid="echo-result" className="inline ml-2">
+                    {clientResults.echoResult?.echo}
+                  </pre>
+                </div>
+                <div>
+                  <span>Method:</span>
+                  <pre data-testid="echo-method" className="inline ml-2">
+                    {clientResults.echoResult?.method}
+                  </pre>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
diff --git a/e2e/react-start/split-exports/src/styles/app.css b/e2e/react-start/split-exports/src/styles/app.css
new file mode 100644
index 0000000000..d4b5078586
--- /dev/null
+++ b/e2e/react-start/split-exports/src/styles/app.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/e2e/react-start/split-exports/src/utils/nested.ts b/e2e/react-start/split-exports/src/utils/nested.ts
new file mode 100644
index 0000000000..cd1b233b2a
--- /dev/null
+++ b/e2e/react-start/split-exports/src/utils/nested.ts
@@ -0,0 +1,62 @@
+/**
+ * This module tests nested imports scenario.
+ * It imports from shared.ts and uses both isomorphic and server-only code internally,
+ * but only exposes isomorphic functions and server functions defined in THIS module.
+ */
+
+import { createServerFn, createIsomorphicFn } from '@tanstack/react-start'
+import { getServerOnlyUserData, formatUserName, APP_NAME } from './shared'
+
+// ============================================================
+// INTERNAL SERVER-ONLY USAGE
+// ============================================================
+
+/**
+ * This server function uses the server-only getServerOnlyUserData internally.
+ * The client can call this server function, but the server-only code
+ * only runs on the server.
+ */
+export const fetchUserProfile = createServerFn()
+  .inputValidator((userId: string) => userId)
+  .handler(async ({ data: userId }) => {
+    // This uses the server-only function internally
+    const userData = getServerOnlyUserData(userId)
+
+    // Transform and return safe data
+    return {
+      id: userData.id,
+      displayName: userData.name,
+      contact: userData.email,
+      appName: APP_NAME,
+    }
+  })
+
+// ============================================================
+// ISOMORPHIC EXPORTS
+// ============================================================
+
+/**
+ * Isomorphic function that computes on both sides.
+ */
+export const computeGreeting = createIsomorphicFn()
+  .server((name: string) => `Hello from the server, ${name}!`)
+  .client((name: string) => `Hello from the client, ${name}!`)
+
+/**
+ * Server function to get greeting from server.
+ */
+export const getServerGreeting = createServerFn()
+  .inputValidator((name: string) => name)
+  .handler(async ({ data: name }) => {
+    return computeGreeting(name)
+  })
+
+/**
+ * Re-export the pure function for use in components.
+ */
+export { formatUserName }
+
+/**
+ * A computed constant.
+ */
+export const GREETING_PREFIX = 'Welcome to ' + APP_NAME
diff --git a/e2e/react-start/split-exports/src/utils/public-api.ts b/e2e/react-start/split-exports/src/utils/public-api.ts
new file mode 100644
index 0000000000..1424cec508
--- /dev/null
+++ b/e2e/react-start/split-exports/src/utils/public-api.ts
@@ -0,0 +1,24 @@
+/**
+ * This module re-exports from shared.ts
+ * Tests that the split-exports plugin handles re-exports correctly.
+ */
+
+// Re-export isomorphic functions (createIsomorphicFn based)
+export {
+  getEnvironment,
+  formatMessage,
+  formatUserName,
+  APP_NAME,
+} from './shared'
+
+// Re-export server functions (createServerFn based - also isomorphic)
+export { getServerEnvironment, getUserById, getAllUsers } from './shared'
+
+// Also re-export with rename
+export { getEnvironment as getEnv } from './shared'
+
+// Note: We intentionally do NOT re-export the server-only exports:
+// - getServerOnlyUserData
+// - getServerOnlyDatabase
+// - serverOnlyConfig
+// This simulates a "public API" module that only exposes safe exports.
diff --git a/e2e/react-start/split-exports/src/utils/server-request.ts b/e2e/react-start/split-exports/src/utils/server-request.ts
new file mode 100644
index 0000000000..ee1dbb980f
--- /dev/null
+++ b/e2e/react-start/split-exports/src/utils/server-request.ts
@@ -0,0 +1,98 @@
+/**
+ * Test: Module with createServerFn that internally uses server-only imports
+ *
+ * This module demonstrates the following pattern:
+ *
+ * 1. Exports isomorphic server functions (createServerFn) that are safe to
+ *    import on the client - the compiler transforms them to fetch calls
+ *
+ * 2. Internally uses `import { getRequest } from '@tanstack/react-start/server'`
+ *    which is a server-only API
+ *
+ * 3. The server-only import is used inside the createServerFn handler, which
+ *    only runs on the server
+ *
+ * When a route imports only the isomorphic exports from this module, the
+ * split-exports plugin should rewrite the import to eliminate the server-only
+ * `getRequest` import from the client bundle through dead code elimination.
+ */
+
+import { createServerFn } from '@tanstack/react-start'
+import { getRequest } from '@tanstack/react-start/server'
+
+// ============================================================
+// SERVER-ONLY CODE - Uses getRequest which is server-only
+// This should NOT be bundled into the client
+// ============================================================
+
+/**
+ * Internal server-only function that extracts info from the current request.
+ * This uses `getRequest()` from @tanstack/react-start/server which is
+ * server-only and would throw if called on the client.
+ */
+function getRequestInfo() {
+  const request = getRequest()
+  return {
+    method: request.method,
+    url: request.url,
+    // Extract just the pathname for easier testing
+    pathname: new URL(request.url).pathname,
+  }
+}
+
+/**
+ * Another server-only function that would break if bundled to client.
+ * This is exported to verify it gets eliminated from client bundles.
+ */
+export function getServerOnlyRequestHeaders() {
+  const request = getRequest()
+  return Object.fromEntries(request.headers.entries())
+}
+
+/**
+ * Server-only constant that should not leak to client.
+ */
+export const SERVER_REQUEST_MARKER =
+  'This string should not appear in client bundles'
+
+// ============================================================
+// ISOMORPHIC CODE - Server Functions (createServerFn)
+// These are safe to import on client - compiler transforms them
+// ============================================================
+
+/**
+ * Server function that returns information about the current request.
+ * This internally calls getRequestInfo() which uses the server-only
+ * getRequest() API. The handler runs on the server, so this is safe.
+ *
+ * When called as a loader:
+ * - method will be "GET"
+ * - url will contain the server function endpoint
+ *
+ * When called as a mutation/action:
+ * - method will be "POST"
+ */
+export const getRequestInfoServerFn = createServerFn().handler(() => {
+  const info = getRequestInfo()
+  return {
+    method: info.method,
+    pathname: info.pathname,
+    // Include a marker to verify this ran on the server
+    executedOn: 'server',
+  }
+})
+
+/**
+ * Server function that echoes back data along with request info.
+ * Tests that server functions with input validators work correctly.
+ */
+export const echoWithRequestInfo = createServerFn()
+  .inputValidator((data: { message: string }) => data)
+  .handler(({ data }) => {
+    const info = getRequestInfo()
+    return {
+      echo: data.message,
+      method: info.method,
+      executedOn: 'server',
+    }
+  })
diff --git a/e2e/react-start/split-exports/src/utils/shared.ts b/e2e/react-start/split-exports/src/utils/shared.ts
new file mode 100644
index 0000000000..b1900f1f20
--- /dev/null
+++ b/e2e/react-start/split-exports/src/utils/shared.ts
@@ -0,0 +1,138 @@
+/**
+ * This module exports BOTH server-only code AND isomorphic code.
+ * The split-exports plugin should allow client code to import only
+ * the isomorphic exports without bundling the server-only code.
+ *
+ * Key test scenario:
+ * - createServerFn exports are ISOMORPHIC (compiler transforms to fetch on client)
+ * - createIsomorphicFn exports are ISOMORPHIC (explicit client/server implementations)
+ * - Plain functions using server-only APIs are SERVER-ONLY (should be eliminated)
+ */
+
+import { createServerFn, createIsomorphicFn } from '@tanstack/react-start'
+
+// ============================================================
+// SERVER-ONLY CODE - This should NOT be bundled into the client
+// ============================================================
+
+/**
+ * Simulates a database connection that only exists on the server.
+ * If this code runs on the client, it would throw an error.
+ */
+export function getServerOnlyDatabase() {
+  // This would typically be something like:
+  // import { db } from './db' // which uses node:fs or similar
+  if (typeof window !== 'undefined') {
+    throw new Error('Database code should not run on the client!')
+  }
+  return {
+    users: {
+      findById: (id: string) => ({
+        id,
+        name: `User ${id}`,
+        email: `user${id}@example.com`,
+        // Secret field that should never be on the client
+        passwordHash: 'secret-hash-should-not-leak',
+      }),
+      findAll: () => [
+        { id: '1', name: 'Alice', email: 'alice@example.com' },
+        { id: '2', name: 'Bob', email: 'bob@example.com' },
+      ],
+    },
+  }
+}
+
+/**
+ * Server-only function that directly uses the database.
+ * This export should be eliminated from client bundles when not imported.
+ */
+export function getServerOnlyUserData(userId: string) {
+  const db = getServerOnlyDatabase()
+  return db.users.findById(userId)
+}
+
+/**
+ * Another server-only export that would break if bundled to client.
+ */
+export const serverOnlyConfig = {
+  databaseUrl: 'postgresql://localhost:5432/mydb',
+  secretKey: 'super-secret-key-should-not-leak',
+  getDatabase: getServerOnlyDatabase,
+}
+
+// ============================================================
+// ISOMORPHIC CODE - Server Functions (createServerFn)
+// These are safe to import on client - compiler transforms them
+// ============================================================
+
+/**
+ * Server function to get the current environment.
+ * Always returns 'server' because the handler runs on the server.
+ * The client gets a fetch call instead.
+ */
+export const getServerEnvironment = createServerFn().handler(() => {
+  return 'server'
+})
+
+/**
+ * Server function to get a user by ID.
+ * Uses the server-only database internally - but that's fine because
+ * the handler only runs on the server.
+ */
+export const getUserById = createServerFn()
+  .inputValidator((userId: string) => userId)
+  .handler(async ({ data: userId }) => {
+    const userData = getServerOnlyUserData(userId)
+    // Return safe data (no password hash)
+    return {
+      id: userData.id,
+      name: userData.name,
+      email: userData.email,
+    }
+  })
+
+/**
+ * Server function to get all users.
+ */
+export const getAllUsers = createServerFn().handler(async () => {
+  const db = getServerOnlyDatabase()
+  return db.users.findAll()
+})
+
+// ============================================================
+// ISOMORPHIC CODE - createIsomorphicFn
+// These have explicit client/server implementations
+// ============================================================
+
+/**
+ * Isomorphic function that has different implementations on client/server.
+ * This is safe to import on the client.
+ */
+export const getEnvironment = createIsomorphicFn()
+  .server(() => 'server')
+  .client(() => 'client')
+
+/**
+ * Isomorphic function with parameters.
+ */
+export const formatMessage = createIsomorphicFn()
+  .server((message: string) => `[SERVER] ${message}`)
+  .client((message: string) => `[CLIENT] ${message}`)
+
+// ============================================================
+// ISOMORPHIC CODE - Pure utilities
+// These work everywhere without transformation
+// ============================================================
+
+/**
+ * Pure isomorphic utility that doesn't need server/client split.
+ * Just a helper function that works everywhere.
+ */
+export function formatUserName(firstName: string, lastName: string): string {
+  return `${firstName} ${lastName}`.trim()
+}
+
+/**
+ * A constant that's safe on both sides.
+ */
+export const APP_NAME = 'Split Exports Test App'
diff --git a/e2e/react-start/split-exports/tests/split-exports-hmr.spec.ts b/e2e/react-start/split-exports/tests/split-exports-hmr.spec.ts
new file mode 100644
index 0000000000..f0d0584a18
--- /dev/null
+++ b/e2e/react-start/split-exports/tests/split-exports-hmr.spec.ts
@@ -0,0 +1,281 @@
+import { expect, type Page } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import * as fs from 'node:fs'
+import * as path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+/**
+ * Split Exports Plugin HMR Tests
+ *
+ * These tests verify that Hot Module Replacement works correctly with the
+ * split-exports plugin. When a source file changes, the correct modules
+ * should be invalidated and the page should update.
+ *
+ * NOTE: These tests run against the dev server, not a production build.
+ * See playwright-hmr.config.ts for the configuration.
+ *
+ * Test scenarios:
+ * 1. Modify shared utility file - all importers should update
+ * 2. Modify isomorphic function - client should see change
+ * 3. Modify server-only code - client should NOT be affected (key feature!)
+ * 4. Add/remove exports - affected importers should update
+ */
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const UTILS_DIR = path.resolve(__dirname, '../src/utils')
+const SHARED_FILE = path.join(UTILS_DIR, 'shared.ts')
+const NESTED_FILE = path.join(UTILS_DIR, 'nested.ts')
+
+// Store original file contents for restoration
+let originalSharedContent: string
+let originalNestedContent: string
+
+test.beforeAll(async () => {
+  // Save original file contents
+  originalSharedContent = await fs.promises.readFile(SHARED_FILE, 'utf-8')
+  originalNestedContent = await fs.promises.readFile(NESTED_FILE, 'utf-8')
+})
+
+test.afterAll(async () => {
+  // Restore original file contents
+  await fs.promises.writeFile(SHARED_FILE, originalSharedContent)
+  await fs.promises.writeFile(NESTED_FILE, originalNestedContent)
+})
+
+test.afterEach(async () => {
+  // Restore files after each test to ensure clean state
+  await fs.promises.writeFile(SHARED_FILE, originalSharedContent)
+  await fs.promises.writeFile(NESTED_FILE, originalNestedContent)
+  // Small delay to ensure file system syncs
+  await new Promise((r) => setTimeout(r, 100))
+})
+
+/**
+ * Helper to wait for HMR update to complete.
+ * Looks for the HMR status indicator or waits for network idle.
+ */
+async function waitForHmr(page: Page, timeout = 5000): Promise<void> {
+  // Wait for any pending HMR connections
+  await page.waitForLoadState('networkidle', { timeout })
+  // Additional small delay for HMR processing
+  await page.waitForTimeout(500)
+}
+
+/**
+ * Helper to modify a file and wait for HMR.
+ */
+async function modifyFileAndWaitForHmr(
+  page: Page,
+  filePath: string,
+  transform: (content: string) => string,
+): Promise<void> {
+  const content = await fs.promises.readFile(filePath, 'utf-8')
+  const modified = transform(content)
+  await fs.promises.writeFile(filePath, modified)
+  await waitForHmr(page)
+}
+
+test.describe('HMR: Isomorphic function changes', () => {
+  test('updating APP_NAME constant reflects in the UI', async ({ page }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Verify initial value
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+
+    // Modify the APP_NAME constant
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        "export const APP_NAME = 'Split Exports Test App'",
+        "export const APP_NAME = 'HMR Updated App Name'",
+      ),
+    )
+
+    // Verify the change is reflected
+    await expect(page.getByTestId('app-name')).toContainText(
+      'HMR Updated App Name',
+    )
+  })
+
+  test('updating formatUserName function reflects in the UI', async ({
+    page,
+  }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Verify initial value
+    await expect(page.getByTestId('formatted-name')).toContainText('John Doe')
+
+    // Modify the formatUserName function to add a prefix
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        'return `${firstName} ${lastName}`.trim()',
+        'return `[HMR] ${firstName} ${lastName}`.trim()',
+      ),
+    )
+
+    // Verify the change is reflected
+    await expect(page.getByTestId('formatted-name')).toContainText('[HMR] John')
+  })
+
+  test('updating getEnvironment client implementation reflects after button click', async ({
+    page,
+  }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // First click to get initial client result
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+    await expect(page.getByTestId('client-env')).toContainText('client')
+
+    // Modify the client implementation
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(".client(() => 'client')", ".client(() => 'hmr-client')"),
+    )
+
+    // Click again and verify the updated behavior
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+    await expect(page.getByTestId('client-env')).toContainText('hmr-client')
+  })
+})
+
+test.describe('HMR: Server-only code changes (should NOT affect client)', () => {
+  test('modifying server-only function does not break client', async ({
+    page,
+  }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Verify initial state works
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+
+    // Modify server-only code - this should NOT cause client errors
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        "passwordHash: 'secret-hash-should-not-leak'",
+        "passwordHash: 'modified-secret-hash'",
+      ),
+    )
+
+    // Client should still work - no errors, still shows content
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+
+    // Client tests should still work
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+    await expect(page.getByTestId('client-env')).toContainText('client')
+  })
+
+  test('modifying server-only config does not affect client imports', async ({
+    page,
+  }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Initial state
+    await expect(page.getByTestId('formatted-name')).toContainText('John Doe')
+
+    // Modify server-only config
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        "databaseUrl: 'postgresql://localhost:5432/mydb'",
+        "databaseUrl: 'postgresql://localhost:5432/modified'",
+      ),
+    )
+
+    // Client functionality should still work perfectly
+    await expect(page.getByTestId('formatted-name')).toContainText('John Doe')
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+    await expect(page.getByTestId('client-message')).toContainText('[CLIENT]')
+  })
+})
+
+test.describe('HMR: Multiple importers', () => {
+  test('change in shared.ts updates importing pages', async ({ page }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        "export const APP_NAME = 'Split Exports Test App'",
+        "export const APP_NAME = 'Multi-Import Test'",
+      ),
+    )
+
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Multi-Import Test',
+    )
+  })
+})
+
+test.describe('HMR: Nested dependency changes', () => {
+  test('change in nested.ts GREETING_PREFIX propagates to importers', async ({
+    page,
+  }) => {
+    await page.goto('/alias-import')
+    await page.waitForLoadState('networkidle')
+
+    // Check initial greeting prefix (computed from APP_NAME)
+    await expect(page.getByTestId('greeting-prefix')).toContainText(
+      'Welcome to Split Exports Test App',
+    )
+
+    // Modify GREETING_PREFIX in nested.ts
+    // Note: This is a computed value, so we need to modify how it's computed
+    await modifyFileAndWaitForHmr(page, NESTED_FILE, (content) =>
+      content.replace(
+        "export const GREETING_PREFIX = 'Welcome to ' + APP_NAME",
+        "export const GREETING_PREFIX = 'HMR Updated: ' + APP_NAME",
+      ),
+    )
+
+    await expect(page.getByTestId('greeting-prefix')).toContainText(
+      'HMR Updated: Split Exports Test App',
+    )
+  })
+
+  test('change in shared.ts APP_NAME propagates through nested.ts to importers', async ({
+    page,
+  }) => {
+    await page.goto('/alias-import')
+    await page.waitForLoadState('networkidle')
+
+    // Initial state uses APP_NAME from shared.ts
+    await expect(page.getByTestId('greeting-prefix')).toContainText(
+      'Welcome to Split Exports Test App',
+    )
+
+    // Modify APP_NAME in shared.ts - should propagate through nested.ts
+    await modifyFileAndWaitForHmr(page, SHARED_FILE, (content) =>
+      content.replace(
+        "export const APP_NAME = 'Split Exports Test App'",
+        "export const APP_NAME = 'Cascading HMR Test'",
+      ),
+    )
+
+    // The GREETING_PREFIX in nested.ts depends on APP_NAME
+    await expect(page.getByTestId('greeting-prefix')).toContainText(
+      'Welcome to Cascading HMR Test',
+    )
+  })
+})
+
+// Note: Error recovery tests are skipped because the test fixture
+// automatically fails on any console errors, and we can't easily bypass
+// that for intentional error scenarios. The main HMR functionality
+// is covered by the other tests.
diff --git a/e2e/react-start/split-exports/tests/split-exports.spec.ts b/e2e/react-start/split-exports/tests/split-exports.spec.ts
new file mode 100644
index 0000000000..8beaab00b1
--- /dev/null
+++ b/e2e/react-start/split-exports/tests/split-exports.spec.ts
@@ -0,0 +1,222 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+/**
+ * Split Exports Plugin E2E Tests
+ *
+ * These tests verify that the split-exports plugin correctly:
+ * 1. Allows importing isomorphic functions from modules with server-only code
+ * 2. Eliminates server-only code from client bundles
+ * 3. Server functions and isomorphic functions work correctly
+ */
+
+test.describe('Direct Import', () => {
+  test('isomorphic functions work correctly on SSR and client', async ({
+    page,
+  }) => {
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Check constants work
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+    await expect(page.getByTestId('formatted-name')).toContainText('John Doe')
+
+    // Check SSR results - isomorphic function returned 'server' during SSR
+    await expect(page.getByTestId('ssr-env')).toContainText('server')
+    // Check that user was loaded from server function
+    await expect(page.getByTestId('ssr-user')).toContainText('User 1')
+
+    // Run client tests
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+
+    // Check client results - isomorphic function returns 'client' on client
+    await expect(page.getByTestId('client-env')).toContainText('client')
+    // Check message was formatted with client prefix
+    await expect(page.getByTestId('client-message')).toContainText('[CLIENT]')
+    // Check server function returns 'server' when called from client
+    await expect(page.getByTestId('server-env')).toContainText('server')
+    // Check server function returned user data
+    await expect(page.getByTestId('server-user')).toContainText('User 2')
+  })
+})
+
+test.describe('Re-export Import', () => {
+  test('re-exported isomorphic functions work correctly', async ({ page }) => {
+    await page.goto('/reexport-import')
+    await page.waitForLoadState('networkidle')
+
+    // Check constants work from re-export
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+
+    // Check SSR results
+    await expect(page.getByTestId('ssr-env')).toContainText('server')
+    // Check users were loaded
+    await expect(page.getByTestId('ssr-users')).toContainText('Alice')
+    await expect(page.getByTestId('ssr-users')).toContainText('Bob')
+
+    // Run client tests
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+
+    // Check client results
+    await expect(page.getByTestId('client-env')).toContainText('client')
+    await expect(page.getByTestId('client-message')).toContainText('[CLIENT]')
+    await expect(page.getByTestId('server-env')).toContainText('server')
+  })
+})
+
+test.describe('Alias Import', () => {
+  test('path alias imports work correctly with split-exports', async ({
+    page,
+  }) => {
+    await page.goto('/alias-import')
+    await page.waitForLoadState('networkidle')
+
+    // Check constants from aliased imports
+    await expect(page.getByTestId('app-name')).toContainText(
+      'Split Exports Test App',
+    )
+    await expect(page.getByTestId('greeting-prefix')).toContainText(
+      'Welcome to Split Exports Test App',
+    )
+    // Check formatUserName (pure function re-exported from shared.ts)
+    await expect(page.getByTestId('formatted-name')).toContainText('Jane Smith')
+
+    // Check SSR results
+    await expect(page.getByTestId('ssr-env')).toContainText('server')
+    await expect(page.getByTestId('ssr-greeting')).toContainText(
+      'Hello from the server',
+    )
+    await expect(page.getByTestId('ssr-user')).toContainText('User 100')
+    // Check profile from nested module (tests internal server-only usage)
+    await expect(page.getByTestId('ssr-profile')).toContainText('User 42')
+
+    // Run client tests
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+
+    // Check client results
+    await expect(page.getByTestId('client-env')).toContainText('client')
+    await expect(page.getByTestId('client-greeting')).toContainText(
+      'Hello from the client',
+    )
+    await expect(page.getByTestId('server-env')).toContainText('server')
+    await expect(page.getByTestId('server-greeting')).toContainText(
+      'Hello from the server',
+    )
+    // Check server profile from nested module
+    await expect(page.getByTestId('server-profile')).toContainText('User 99')
+  })
+})
+
+test.describe('Server Request Import', () => {
+  /**
+   * This test verifies a critical bug fix in the split-exports plugin:
+   *
+   * When a route file is processed with `?tss-split-exports=Route`, imports
+   * inside that file should ALSO be rewritten with the split-exports query.
+   * Previously, the plugin would skip import rewriting for files that already
+   * had the query, causing server-only imports to leak into the client bundle.
+   *
+   * The test scenario:
+   * 1. server-request.ts exports createServerFn (isomorphic) and uses
+   *    `import { getRequest } from '@tanstack/react-start/server'` internally
+   * 2. getRequest is server-only and should NOT be in client bundle
+   * 3. server-request-import.tsx imports only the createServerFn
+   * 4. The import should be rewritten to only include the server function,
+   *    eliminating the server-only getRequest import from the client
+   */
+  test('server function using internal getRequest works correctly', async ({
+    page,
+  }) => {
+    await page.goto('/server-request-import')
+    await page.waitForLoadState('networkidle')
+
+    // Check SSR/loader results
+    // Loaders use GET requests
+    await expect(page.getByTestId('loader-method')).toContainText('GET')
+    await expect(page.getByTestId('loader-executed-on')).toContainText('server')
+
+    // Run client tests
+    await page.getByTestId('run-client-tests-btn').click()
+    await page.waitForLoadState('networkidle')
+
+    // Check client results
+    // Client-side calls also use GET requests (with payload in query params)
+    await expect(page.getByTestId('client-method')).toContainText('GET')
+    await expect(page.getByTestId('client-executed-on')).toContainText('server')
+    await expect(page.getByTestId('echo-result')).toContainText(
+      'Hello from client!',
+    )
+    await expect(page.getByTestId('echo-method')).toContainText('GET')
+  })
+})
+
+test.describe('No server-only code leaks to client', () => {
+  test('client bundle does not contain server-only code markers', async ({
+    page,
+  }) => {
+    // Navigate to a page to load client bundles
+    await page.goto('/direct-import')
+    await page.waitForLoadState('networkidle')
+
+    // Get all script sources
+    const scripts = await page.evaluate(() => {
+      const scriptElements = Array.from(
+        document.querySelectorAll('script[src]'),
+      )
+      return scriptElements.map((s) => s.getAttribute('src')).filter(Boolean)
+    })
+
+    // For each script, fetch and check it doesn't contain server-only markers
+    for (const scriptSrc of scripts) {
+      if (!scriptSrc || scriptSrc.includes('node_modules')) continue
+
+      const response = await page.request.get(scriptSrc)
+      const content = await response.text()
+
+      // These strings should NOT appear in client bundles
+      expect(content).not.toContain('passwordHash')
+      expect(content).not.toContain('super-secret-key-should-not-leak')
+      expect(content).not.toContain('postgresql://localhost:5432')
+      expect(content).not.toContain(
+        'Database code should not run on the client',
+      )
+    }
+  })
+
+  test('server-request-import page does not leak getRequest import to client', async ({
+    page,
+  }) => {
+    // This test verifies the bug fix for imports inside files with ?tss-split-exports
+    // Navigate to the server-request-import page to load its client bundles
+    await page.goto('/server-request-import')
+    await page.waitForLoadState('networkidle')
+
+    // Get all script sources
+    const scripts = await page.evaluate(() => {
+      const scriptElements = Array.from(
+        document.querySelectorAll('script[src]'),
+      )
+      return scriptElements.map((s) => s.getAttribute('src')).filter(Boolean)
+    })
+
+    // For each script, fetch and check it doesn't contain server-only markers
+    for (const scriptSrc of scripts) {
+      if (!scriptSrc || scriptSrc.includes('node_modules')) continue
+
+      const response = await page.request.get(scriptSrc)
+      const content = await response.text()
+
+      // These strings from server-request.ts should NOT appear in client bundles
+      expect(content).not.toContain(
+        'This string should not appear in client bundles',
+      )
+    }
+  })
+})
diff --git a/e2e/react-start/split-exports/tsconfig.json b/e2e/react-start/split-exports/tsconfig.json
new file mode 100644
index 0000000000..3a9fb7cd71
--- /dev/null
+++ b/e2e/react-start/split-exports/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "include": ["**/*.ts", "**/*.tsx"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true
+  }
+}
diff --git a/e2e/react-start/split-exports/vite.config.ts b/e2e/react-start/split-exports/vite.config.ts
new file mode 100644
index 0000000000..ae06492a8e
--- /dev/null
+++ b/e2e/react-start/split-exports/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+
+export default defineConfig({
+  server: {
+    port: Number(process.env.PORT) || 3000,
+  },
+  plugins: [
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    viteReact(),
+  ],
+})
diff --git a/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts b/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts
index 2af5736775..c4d3967f09 100644
--- a/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts
+++ b/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts
@@ -76,7 +76,7 @@ export const localFnPOST = localFnFactory({ method: 'POST' })
 export const fakeFn = createFakeFn().handler(async () => {
   return {
     name: 'fakeFn',
-    window,
+    window: typeof window !== 'undefined' ? window : 'no window object',
   }
 })
 
diff --git a/e2e/solid-start/server-functions/src/routes/factory/index.tsx b/e2e/solid-start/server-functions/src/routes/factory/index.tsx
index edbf65eb41..e60f9bca34 100644
--- a/e2e/solid-start/server-functions/src/routes/factory/index.tsx
+++ b/e2e/solid-start/server-functions/src/routes/factory/index.tsx
@@ -127,7 +127,7 @@ const functions = {
     type: 'localFn',
     expected: {
       name: 'fakeFn',
-      window,
+      window: typeof window !== 'undefined' ? window : 'no window object',
     },
   },
 } satisfies Record<string, TestCase>
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
index 1fb95ee078..07b4015b20 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
@@ -57,6 +57,7 @@ export class ServerFnCompiler {
   private moduleCache = new Map<string, ModuleInfo>()
   private initialized = false
   private validLookupKinds: Set<LookupKind>
+
   constructor(
     private options: {
       env: 'client' | 'server'
@@ -64,7 +65,10 @@ export class ServerFnCompiler {
       lookupConfigurations: Array<LookupConfig>
       lookupKinds: Set<LookupKind>
       loadModule: (id: string) => Promise<void>
-      resolveId: (id: string, importer?: string) => Promise<string | null>
+      resolveId: (
+        id: string,
+        importer: string | undefined,
+      ) => Promise<string | null>
     },
   ) {
     this.validLookupKinds = options.lookupKinds
@@ -219,7 +223,7 @@ export class ServerFnCompiler {
       kind: LookupKind
     }> = []
     for (const handler of candidates) {
-      const kind = await this.resolveExprKind(handler, id)
+      const kind = await this.resolveExprKind(handler, id, new Set())
       if (this.validLookupKinds.has(kind as LookupKind)) {
         toRewrite.push({ callExpression: handler, kind: kind as LookupKind })
       }
@@ -293,7 +297,7 @@ export class ServerFnCompiler {
   private async resolveIdentifierKind(
     ident: string,
     id: string,
-    visited = new Set<string>(),
+    visited: Set<string>,
   ): Promise<Kind> {
     const info = await this.getModuleInfo(id)
 
@@ -321,7 +325,7 @@ export class ServerFnCompiler {
   private async resolveBindingKind(
     binding: Binding,
     fileId: string,
-    visited = new Set<string>(),
+    visited: Set<string>,
   ): Promise<Kind> {
     if (binding.resolvedKind) {
       return binding.resolvedKind
@@ -367,7 +371,7 @@ export class ServerFnCompiler {
   private async resolveExprKind(
     expr: t.Expression | null,
     fileId: string,
-    visited = new Set<string>(),
+    visited: Set<string>,
   ): Promise<Kind> {
     if (!expr) {
       return 'None'
@@ -418,7 +422,7 @@ export class ServerFnCompiler {
   private async resolveCalleeKind(
     callee: t.Expression,
     fileId: string,
-    visited = new Set<string>(),
+    visited: Set<string>,
   ): Promise<Kind> {
     if (t.isIdentifier(callee)) {
       return this.resolveIdentifierKind(callee.name, fileId, visited)
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
index 7afd4f9432..bbc305a095 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
@@ -1,13 +1,12 @@
+import type { Environment } from 'vite'
 import { TRANSFORM_ID_REGEX } from '../constants'
+import type { ModuleLoaderApi } from '../module-loader-plugin/plugin'
+import { findModuleLoaderApi, stripQueryString } from '../plugin-utils'
 import { ServerFnCompiler } from './compiler'
 import type { LookupConfig, LookupKind } from './compiler'
 import type { CompileStartFrameworkOptions } from '../start-compiler-plugin/compilers'
 import type { PluginOption } from 'vite'
 
-function cleanId(id: string): string {
-  return id.split('?')[0]!
-}
-
 const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>> = {
   client: new Set(['Middleware', 'ServerFn'] as const),
   server: new Set(['ServerFn'] as const),
@@ -38,13 +37,14 @@ const getLookupConfigurationsForEnv = (
     return [createServerFnConfig]
   }
 }
-const SERVER_FN_LOOKUP = 'server-fn-module-lookup'
+
 export function createServerFnPlugin(opts: {
   framework: CompileStartFrameworkOptions
   directive: string
   environments: Array<{ name: string; type: 'client' | 'server' }>
 }): PluginOption {
-  const compilers: Record<string /* envName */, ServerFnCompiler> = {}
+  // Store compilers per environment name to handle concurrent transforms correctly
+  const compilers: Record<string, ServerFnCompiler> = {}
 
   function perEnvServerFnPlugin(environment: {
     name: string
@@ -56,6 +56,18 @@ export function createServerFnPlugin(opts: {
         ? [/\.\s*handler\(/, /\.\s*createMiddleware\(\)/]
         : [/\.\s*handler\(/]
 
+    let loaderApi: ModuleLoaderApi | undefined
+    let viteEnv: Environment | undefined
+    let ctxLoad:
+      | ((options: { id: string }) => Promise<{ code: string | null }>)
+      | undefined
+    let ctxResolve:
+      | ((
+          source: string,
+          importer?: string,
+        ) => Promise<{ id: string; external?: boolean | 'absolute' } | null>)
+      | undefined
+
     return {
       name: `tanstack-start-core::server-fn:${environment.name}`,
       enforce: 'pre',
@@ -65,7 +77,6 @@ export function createServerFnPlugin(opts: {
       transform: {
         filter: {
           id: {
-            exclude: new RegExp(`${SERVER_FN_LOOKUP}$`),
             include: TRANSFORM_ID_REGEX,
           },
           code: {
@@ -73,8 +84,16 @@ export function createServerFnPlugin(opts: {
           },
         },
         async handler(code, id) {
-          let compiler = compilers[this.environment.name]
+          const env = this.environment
+          const envName = env.name
+
+          let compiler = compilers[envName]
           if (!compiler) {
+            loaderApi = findModuleLoaderApi(env.plugins)
+            viteEnv = env
+            ctxLoad = this.load.bind(this)
+            ctxResolve = this.resolve.bind(this)
+
             compiler = new ServerFnCompiler({
               env: environment.type,
               directive: opts.directive,
@@ -83,44 +102,31 @@ export function createServerFnPlugin(opts: {
                 environment.type,
                 opts.framework,
               ),
-              loadModule: async (id: string) => {
-                if (this.environment.mode === 'build') {
-                  const loaded = await this.load({ id })
-                  if (!loaded.code) {
-                    throw new Error(`could not load module ${id}`)
-                  }
-                  compiler!.ingestModule({ code: loaded.code, id })
-                } else if (this.environment.mode === 'dev') {
-                  /**
-                   * in dev, vite does not return code from `ctx.load()`
-                   * so instead, we need to take a different approach
-                   * we must force vite to load the module and run it through the vite plugin pipeline
-                   * we can do this by using the `fetchModule` method
-                   * the `captureServerFnModuleLookupPlugin` captures the module code via its transform hook and invokes analyzeModuleAST
-                   */
-                  await this.environment.fetchModule(
-                    id + '?' + SERVER_FN_LOOKUP,
-                  )
-                } else {
-                  throw new Error(
-                    `could not load module ${id}: unknown environment mode ${this.environment.mode}`,
-                  )
-                }
+              loadModule: async (moduleId: string) => {
+                const ctxLoadForEnv =
+                  viteEnv!.mode === 'build' ? ctxLoad : undefined
+                const moduleCode = await loaderApi!.loadModuleCode(
+                  viteEnv!,
+                  moduleId,
+                  ctxLoadForEnv,
+                )
+                compiler!.ingestModule({ code: moduleCode, id: moduleId })
               },
-              resolveId: async (source: string, importer?: string) => {
-                const r = await this.resolve(source, importer)
-                if (r) {
-                  if (!r.external) {
-                    return cleanId(r.id)
-                  }
+              resolveId: async (
+                source: string,
+                importer: string | undefined,
+              ) => {
+                const r = await ctxResolve!(source, importer)
+                if (r && !r.external) {
+                  return stripQueryString(r.id)
                 }
                 return null
               },
             })
-            compilers[this.environment.name] = compiler
+            compilers[envName] = compiler
           }
 
-          id = cleanId(id)
+          id = stripQueryString(id)
           const result = await compiler.compile({ id, code })
           return result
         },
@@ -145,24 +151,5 @@ export function createServerFnPlugin(opts: {
     }
   }
 
-  return [
-    ...opts.environments.map(perEnvServerFnPlugin),
-    {
-      name: 'tanstack-start-core:capture-server-fn-module-lookup',
-      // we only need this plugin in dev mode
-      apply: 'serve',
-      applyToEnvironment(env) {
-        return !!opts.environments.find((e) => e.name === env.name)
-      },
-      transform: {
-        filter: {
-          id: new RegExp(`${SERVER_FN_LOOKUP}$`),
-        },
-        handler(code, id) {
-          const compiler = compilers[this.environment.name]
-          compiler?.ingestModule({ code, id: cleanId(id) })
-        },
-      },
-    },
-  ]
+  return opts.environments.map(perEnvServerFnPlugin)
 }
diff --git a/packages/start-plugin-core/src/module-loader-plugin/index.ts b/packages/start-plugin-core/src/module-loader-plugin/index.ts
new file mode 100644
index 0000000000..f9a71b9f71
--- /dev/null
+++ b/packages/start-plugin-core/src/module-loader-plugin/index.ts
@@ -0,0 +1,2 @@
+export { moduleLoaderPlugin, MODULE_LOADER_QUERY_KEY } from './plugin'
+export type { ModuleLoaderApi } from './plugin'
diff --git a/packages/start-plugin-core/src/module-loader-plugin/plugin.ts b/packages/start-plugin-core/src/module-loader-plugin/plugin.ts
new file mode 100644
index 0000000000..1a005073d2
--- /dev/null
+++ b/packages/start-plugin-core/src/module-loader-plugin/plugin.ts
@@ -0,0 +1,189 @@
+import type { DevEnvironment, Environment, Plugin, PluginOption } from 'vite'
+import { isDevEnvironment } from '../plugin-utils'
+
+export const MODULE_LOADER_QUERY_KEY = 'tss-module-load'
+
+/**
+ * Interface for the module loader API exposed to other plugins.
+ */
+export interface ModuleLoaderApi {
+  /**
+   * Load a module's transformed code by its resolved ID.
+   * Works in both dev and build modes.
+   *
+   * @param env - The environment to load the module in
+   * @param id - The resolved module ID to load
+   * @param ctxLoad - In build mode, pass ctx.load bound to ctx
+   * @returns The transformed module code
+   */
+  loadModuleCode(
+    env: Environment,
+    id: string,
+    ctxLoad?: (options: { id: string }) => Promise<{ code: string | null }>,
+  ): Promise<string>
+}
+
+/**
+ * Plugin that provides module loading capabilities to other plugins.
+ * This plugin must be added before plugins that need to load module code.
+ *
+ * In build mode, uses Vite's ctx.load() which returns code directly.
+ * In dev mode, uses fetchModule with a capture plugin since ctx.load()
+ * doesn't return code in dev mode.
+ *
+ * @example
+ * ```typescript
+ * // In another plugin:
+ * const loaderPlugin = this.environment.plugins.find(
+ *   p => p.name === 'tanstack-module-loader'
+ * )
+ * const code = await loaderPlugin.api.loadModuleCode(this, resolvedId)
+ * ```
+ */
+export function moduleLoaderPlugin(): PluginOption {
+  // Cache of loaded module code per environment
+  // Map: envName -> (id -> code)
+  const moduleCache = new Map<string, Map<string, string>>()
+
+  // Pending load requests for dev mode async loading
+  // Map: envName -> (id -> { resolve, reject })
+  const pendingLoads = new Map<
+    string,
+    Map<
+      string,
+      {
+        resolve: (code: string) => void
+        reject: (err: Error) => void
+      }
+    >
+  >()
+
+  function getEnvCache(envName: string): Map<string, string> {
+    let cache = moduleCache.get(envName)
+    if (!cache) {
+      cache = new Map()
+      moduleCache.set(envName, cache)
+    }
+    return cache
+  }
+
+  function getEnvPendingLoads(
+    envName: string,
+  ): Map<
+    string,
+    { resolve: (code: string) => void; reject: (err: Error) => void }
+  > {
+    let pending = pendingLoads.get(envName)
+    if (!pending) {
+      pending = new Map()
+      pendingLoads.set(envName, pending)
+    }
+    return pending
+  }
+
+  const mainPlugin: Plugin = {
+    name: 'tanstack-module-loader',
+
+    api: {
+      async loadModuleCode(
+        env: Environment,
+        id: string,
+        ctxLoad?: (options: { id: string }) => Promise<{ code: string | null }>,
+      ): Promise<string> {
+        const envName = env.name
+
+        // Check cache first
+        const envCache = getEnvCache(envName)
+        const cached = envCache.get(id)
+        if (cached !== undefined) {
+          return cached
+        }
+
+        if (env.mode === 'build') {
+          // In build mode, use the provided ctxLoad
+          if (!ctxLoad) {
+            throw new Error(
+              `loadModuleCode requires ctxLoad in build mode for module: ${id}`,
+            )
+          }
+          const loaded = await ctxLoad({ id })
+          if (!loaded.code) {
+            throw new Error(`Could not load module: ${id}`)
+          }
+          // Cache it
+          envCache.set(id, loaded.code)
+          return loaded.code
+        } else if (isDevEnvironment(env)) {
+          // In dev mode, use fetchModule with capture plugin
+          // The fetchModule triggers Vite to load and transform the module,
+          // and our capture plugin intercepts it to get the code
+          const devEnv: DevEnvironment = env
+          return new Promise((resolve, reject) => {
+            // Register pending load
+            const envPending = getEnvPendingLoads(envName)
+            envPending.set(id, { resolve, reject })
+
+            // Trigger fetch - the capture plugin will intercept and resolve
+            devEnv
+              .fetchModule(id + '?' + MODULE_LOADER_QUERY_KEY)
+              .catch((err: Error) => {
+                // Clean up pending on error
+                envPending.delete(id)
+                reject(err)
+              })
+          })
+        } else {
+          throw new Error(
+            `Could not load module ${id}: unsupported environment mode`,
+          )
+        }
+      },
+    } satisfies ModuleLoaderApi,
+
+    // Invalidate cache on HMR
+    hotUpdate(ctx) {
+      const envCache = moduleCache.get(this.environment.name)
+      if (envCache) {
+        for (const mod of ctx.modules) {
+          if (mod.id) {
+            envCache.delete(mod.id)
+          }
+        }
+      }
+    },
+  }
+
+  // Capture plugin for dev mode - intercepts the fetchModule request
+  const capturePlugin: Plugin = {
+    name: 'tanstack-module-loader:capture',
+    apply: 'serve',
+
+    transform: {
+      filter: {
+        id: new RegExp(`\\?${MODULE_LOADER_QUERY_KEY}$`),
+      },
+      handler(code, id) {
+        // Remove query parameter to get clean ID
+        const cleanId = id.replace('?' + MODULE_LOADER_QUERY_KEY, '')
+        const envName = this.environment.name
+
+        // Cache the code
+        const envCache = getEnvCache(envName)
+        envCache.set(cleanId, code)
+
+        // Resolve pending load if any
+        const envPending = pendingLoads.get(envName)
+        const pending = envPending?.get(cleanId)
+        if (pending) {
+          pending.resolve(code)
+          envPending!.delete(cleanId)
+        }
+
+        // Return null to not affect the module transform
+        return null
+      },
+    },
+  }
+
+  return [mainPlugin, capturePlugin]
+}
diff --git a/packages/start-plugin-core/src/plugin-utils.ts b/packages/start-plugin-core/src/plugin-utils.ts
new file mode 100644
index 0000000000..a377ef808e
--- /dev/null
+++ b/packages/start-plugin-core/src/plugin-utils.ts
@@ -0,0 +1,50 @@
+import type { DevEnvironment, Environment, Plugin, Rollup } from 'vite'
+import type { ModuleLoaderApi } from './module-loader-plugin'
+
+// ============ Types ============
+
+/**
+ * Plugin context type that works across different TypeScript/Vite versions.
+ * Extends Rollup's TransformPluginContext with Vite's environment additions.
+ *
+ * Uses Vite's Environment type which is a union of DevEnvironment, BuildEnvironment,
+ * and UnknownEnvironment.
+ */
+export type PluginContext = Rollup.TransformPluginContext & {
+  environment: Environment
+}
+
+/**
+ * Type guard to check if an environment is a DevEnvironment (has fetchModule).
+ */
+export function isDevEnvironment(env: Environment): env is DevEnvironment {
+  return env.mode === 'dev'
+}
+
+// ============ Utilities ============
+
+/**
+ * Strip query string from a module ID to get the clean file path.
+ */
+export function stripQueryString(id: string): string {
+  return id.split('?')[0]!
+}
+
+/**
+ * Find the module loader API from environment plugins.
+ * Throws if the module loader plugin is not found.
+ */
+export function findModuleLoaderApi(
+  envPlugins: ReadonlyArray<Plugin>,
+): ModuleLoaderApi {
+  const loaderPlugin = envPlugins.find(
+    (p): p is Plugin & { api: ModuleLoaderApi } =>
+      p.name === 'tanstack-module-loader' && !!p.api,
+  )
+  if (!loaderPlugin) {
+    throw new Error(
+      'Module loader plugin not found. Ensure moduleLoaderPlugin() is added before other TanStack Start plugins.',
+    )
+  }
+  return loaderPlugin.api
+}
diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts
index 310d7309cf..e84b8957f8 100644
--- a/packages/start-plugin-core/src/plugin.ts
+++ b/packages/start-plugin-core/src/plugin.ts
@@ -20,6 +20,8 @@ import {
 } from './output-directory'
 import { postServerBuild } from './post-server-build'
 import { createServerFnPlugin } from './create-server-fn-plugin/plugin'
+import { moduleLoaderPlugin } from './module-loader-plugin/plugin'
+import { splitExportsPlugin } from './split-exports-plugin/plugin'
 import type { ViteEnvironmentNames } from './constants'
 import type {
   TanStackStartInputConfig,
@@ -360,6 +362,9 @@ export function TanStackStartVitePluginCore(
       },
     },
     tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts),
+    // Module loader plugin provides shared module loading capabilities
+    // Must be before plugins that need to load module code (split-exports, create-server-fn)
+    moduleLoaderPlugin(),
     // N.B. TanStackStartCompilerPlugin must be before the TanStackServerFnPlugin
     startCompilerPlugin({ framework: corePluginOpts.framework, environments }),
     createServerFnPlugin({
@@ -367,6 +372,13 @@ export function TanStackStartVitePluginCore(
       directive,
       environments,
     }),
+    // Split exports plugin rewrites imports to enable better dead code elimination
+    // Must run AFTER server function plugins so that .handler() calls are already transformed
+    // before split-exports creates module variants with query strings
+    splitExportsPlugin({
+      enabled: startPluginOpts?.importOptimization?.enabled ?? true,
+      getConfig,
+    }),
 
     TanStackServerFnPlugin({
       // This is the ID that will be available to look up and import
diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts
index 7dec32effd..f723978c58 100644
--- a/packages/start-plugin-core/src/schema.ts
+++ b/packages/start-plugin-core/src/schema.ts
@@ -192,6 +192,12 @@ const tanstackStartOptionsSchema = z
       .and(pagePrerenderOptionsSchema.optional())
       .optional(),
     spa: spaSchema.optional(),
+    importOptimization: z
+      .object({
+        enabled: z.boolean().optional().default(true),
+      })
+      .optional()
+      .default({}),
     vite: z
       .object({ installDevServerMiddleware: z.boolean().optional() })
       .optional(),
diff --git a/packages/start-plugin-core/src/split-exports-plugin/README.md b/packages/start-plugin-core/src/split-exports-plugin/README.md
new file mode 100644
index 0000000000..b2e1a1a9ab
--- /dev/null
+++ b/packages/start-plugin-core/src/split-exports-plugin/README.md
@@ -0,0 +1,124 @@
+# Split-Exports Plugin
+
+A Vite plugin that automatically optimizes imports by rewriting them with query strings to enable dead code elimination. This prevents server-only code from leaking into client bundles.
+
+## How It Works
+
+The plugin operates in two phases:
+
+### Phase 1: Import Rewriting
+
+When a file imports from another module, the plugin rewrites the import to include a query parameter listing which exports are actually used.
+
+**Before:**
+
+```ts
+// routes/users.tsx
+import { formatUser, getUser } from '../utils/user'
+```
+
+**After:**
+
+```ts
+// routes/users.tsx
+import {
+  formatUser,
+  getUser,
+} from '../utils/user?tss-split-exports=formatUser,getUser'
+```
+
+### Phase 2: Export Transformation
+
+When a module is loaded with the `?tss-split-exports` query, the plugin transforms it to only export the requested symbols. Unrequested exports are converted to local declarations, and dead code elimination removes any code that becomes unreferenced.
+
+## Example: Separating Server and Client Code
+
+Consider a utility file that exports both isomorphic and server-only code:
+
+```ts
+// utils/user.ts
+import { db } from './db'
+
+// Isomorphic - safe for client
+export const formatUser = (user: { name: string }) => {
+  return user.name.toUpperCase()
+}
+
+// Server-only - uses database
+export const getUser = async (id: string) => {
+  return db.users.findOne({ id })
+}
+
+// Server-only - uses database
+export const deleteUser = async (id: string) => {
+  return db.users.delete({ id })
+}
+```
+
+And a route that only uses `formatUser`:
+
+```ts
+// routes/profile.tsx
+import { formatUser } from '../utils/user'
+
+export const Route = createFileRoute('/profile')({
+  component: () => <div>{formatUser({ name: 'john' })}</div>,
+})
+```
+
+**What happens:**
+
+1. The import is rewritten to:
+
+   ```ts
+   import { formatUser } from '../utils/user?tss-split-exports=formatUser'
+   ```
+
+2. When `utils/user.ts?tss-split-exports=formatUser` is loaded, it's transformed to:
+
+   ```ts
+   // Isomorphic - safe for client
+   export const formatUser = (user: { name: string }) => {
+     return user.name.toUpperCase()
+   }
+   ```
+
+3. The `db` import, `getUser`, and `deleteUser` are all eliminated because they're no longer referenced.
+
+**Result:** The client bundle only contains `formatUser`. The database code never reaches the browser.
+
+## What Gets Skipped
+
+The plugin intentionally skips certain import/export patterns:
+
+| Pattern                  | Example                               | Reason                                          |
+| ------------------------ | ------------------------------------- | ----------------------------------------------- |
+| npm packages             | `import { useState } from 'react'`    | External packages handle their own tree-shaking |
+| Type-only imports        | `import type { User } from './types'` | Types are erased at compile time                |
+| Namespace imports        | `import * as utils from './utils'`    | Can't determine which exports are used          |
+| Side-effect-only imports | `import './polyfill'`                 | No specifiers to track                          |
+| Wildcard re-exports      | `export * from './other'`             | Can't enumerate exports statically              |
+
+## Configuration
+
+```ts
+splitExportsPlugin({
+  // Enable/disable the plugin (default: true)
+  enabled: true,
+
+  // Paths to exclude from transformation
+  exclude: ['generated', /\.test\./],
+
+  // Function to get resolved Start config (for srcDirectory filtering)
+  getConfig: () => ({ resolvedStartConfig }),
+})
+```
+
+Debug logging can be enabled by setting the environment variable `TSR_VITE_DEBUG=split-exports` or `TSR_VITE_DEBUG=true`.
+
+## File Structure
+
+- `plugin.ts` - Main Vite plugin with four sub-plugins (import rewriter, resolver, export transformer, HMR)
+- `compiler.ts` - AST transformation logic using Babel
+- `query-utils.ts` - Query string parsing and manipulation for module IDs
+- `plugin-utils.ts` - Utility functions for path handling and filtering
diff --git a/packages/start-plugin-core/src/split-exports-plugin/compiler.ts b/packages/start-plugin-core/src/split-exports-plugin/compiler.ts
new file mode 100644
index 0000000000..6a1491a2ea
--- /dev/null
+++ b/packages/start-plugin-core/src/split-exports-plugin/compiler.ts
@@ -0,0 +1,594 @@
+import * as t from '@babel/types'
+import babel from '@babel/core'
+import {
+  deadCodeElimination,
+  findReferencedIdentifiers,
+} from 'babel-dead-code-elimination'
+import { generateFromAst, parseAst } from '@tanstack/router-utils'
+import { appendSplitExportsQuery, hasSplitExportsQuery } from './query-utils'
+import type { GeneratorResult, ParseAstResult } from '@tanstack/router-utils'
+
+/**
+ * Result of extracting imports from a module.
+ * Includes the parsed AST for reuse in subsequent transformations.
+ */
+export interface ExtractImportsResult {
+  /** Map of source module -> set of imported names */
+  imports: Map<string, Set<string>>
+  /** The parsed AST, can be reused by transformImports to avoid double parsing */
+  ast: ParseAstResult
+}
+
+/**
+ * Check if an import source looks like a bare module specifier (npm package).
+ * Examples: 'lodash', '@tanstack/react-router', 'react-dom/client'
+ */
+export function isBareModuleSpecifier(source: string): boolean {
+  // Relative or absolute imports
+  if (source.startsWith('.') || source.startsWith('/')) {
+    return false
+  }
+
+  // Common alias prefixes
+  if (source.startsWith('~') || source.startsWith('#')) {
+    return false
+  }
+
+  // Scoped packages (@scope/package) vs aliases (@/path)
+  if (source.startsWith('@')) {
+    const match = source.match(/^@([^/]+)\//)
+    if (!match) return false // Just @ with no slash - probably an alias
+
+    const scope = match[1]
+    // Valid npm scope: non-empty, lowercase letters, numbers, hyphens
+    return !!scope && /^[a-z0-9-]+$/.test(scope)
+  }
+
+  // Everything else is likely a bare module (lodash, react, etc.)
+  return true
+}
+
+/**
+ * Check if a module exports any classes.
+ * This is used to skip the split-exports optimization for modules with class exports,
+ * as class identity would be lost when the module is loaded with different query strings.
+ *
+ * Detects:
+ * - export class Foo {}
+ * - export default class Foo {}
+ * - export default class {}
+ * - class Foo {}; export { Foo }
+ * - class Foo {}; export { Foo as Bar }
+ * - export const Foo = class {}
+ * - export const Bar = class BarClass {}
+ * - const Foo = class {}; export { Foo }
+ */
+export function hasClassExports(code: string): boolean {
+  // Quick string check for early bailout
+  if (!code.includes('class ')) {
+    return false
+  }
+
+  const ast = parseAst({ code })
+
+  // Collect names of top-level class declarations and class expression variables
+  const localClassNames = new Set<string>()
+
+  for (const node of ast.program.body) {
+    // Track local class declarations: class Foo {}
+    if (t.isClassDeclaration(node) && node.id) {
+      localClassNames.add(node.id.name)
+    }
+
+    // Track local class expressions: const Foo = class {}
+    if (t.isVariableDeclaration(node)) {
+      for (const declarator of node.declarations) {
+        if (
+          t.isClassExpression(declarator.init) &&
+          t.isIdentifier(declarator.id)
+        ) {
+          localClassNames.add(declarator.id.name)
+        }
+      }
+    }
+
+    // 1. export class Foo {}
+    if (t.isExportNamedDeclaration(node)) {
+      if (t.isClassDeclaration(node.declaration)) {
+        return true
+      }
+
+      // 2. export const Foo = class {}
+      if (t.isVariableDeclaration(node.declaration)) {
+        for (const declarator of node.declaration.declarations) {
+          if (t.isClassExpression(declarator.init)) {
+            return true
+          }
+        }
+      }
+    }
+
+    // 3. export default class {} or export default class Foo {}
+    if (t.isExportDefaultDeclaration(node)) {
+      if (
+        t.isClassDeclaration(node.declaration) ||
+        t.isClassExpression(node.declaration)
+      ) {
+        return true
+      }
+    }
+  }
+
+  // 4. Check for export { Foo } where Foo is a local class
+  for (const node of ast.program.body) {
+    if (t.isExportNamedDeclaration(node) && !node.source) {
+      // Only check local exports, not re-exports like export { Foo } from './other'
+      for (const specifier of node.specifiers) {
+        if (t.isExportSpecifier(specifier) && t.isIdentifier(specifier.local)) {
+          if (localClassNames.has(specifier.local.name)) {
+            return true
+          }
+        }
+      }
+    }
+  }
+
+  return false
+}
+
+/**
+ * Parse a module and extract all import specifiers from it.
+ * Returns a map of source module -> set of imported names, plus the parsed AST.
+ * Type-only imports and namespace imports are ignored.
+ * Bare module specifiers (npm packages) are skipped.
+ *
+ * The returned AST can be passed to `transformImports` to avoid double parsing.
+ */
+export function extractImportsFromModule(code: string): ExtractImportsResult {
+  const ast = parseAst({ code })
+  const imports = new Map<string, Set<string>>()
+
+  for (const node of ast.program.body) {
+    if (!t.isImportDeclaration(node)) {
+      continue
+    }
+
+    // Skip type-only imports entirely (import type { ... })
+    if (node.importKind === 'type') {
+      continue
+    }
+
+    const source = node.source.value
+
+    // Skip bare module specifiers (npm packages)
+    if (isBareModuleSpecifier(source)) {
+      continue
+    }
+
+    // Skip imports that already have our query parameter
+    if (hasSplitExportsQuery(source)) {
+      continue
+    }
+
+    // Check for namespace imports first - if present, skip this source entirely
+    const hasNamespaceImport = node.specifiers.some((spec) =>
+      t.isImportNamespaceSpecifier(spec),
+    )
+    if (hasNamespaceImport) {
+      continue
+    }
+
+    // Skip side-effect only imports (import './foo')
+    if (node.specifiers.length === 0) {
+      continue
+    }
+
+    let importedNames = imports.get(source)
+    if (!importedNames) {
+      importedNames = new Set()
+      imports.set(source, importedNames)
+    }
+
+    for (const specifier of node.specifiers) {
+      // Skip type-only specifiers
+      if (t.isImportSpecifier(specifier) && specifier.importKind === 'type') {
+        continue
+      }
+
+      if (t.isImportSpecifier(specifier)) {
+        const importedName = t.isIdentifier(specifier.imported)
+          ? specifier.imported.name
+          : specifier.imported.value
+        importedNames.add(importedName)
+      } else if (t.isImportDefaultSpecifier(specifier)) {
+        importedNames.add('default')
+      }
+      // Note: namespace imports are already filtered out above
+    }
+  }
+
+  // Remove entries with no value imports (all type-only specifiers)
+  // We don't need to transform these - Vite will handle them
+  for (const [source, names] of imports) {
+    if (names.size === 0) {
+      imports.delete(source)
+    }
+  }
+
+  return { imports, ast }
+}
+
+/**
+ * Options for transforming imports.
+ */
+export interface TransformImportsOptions {
+  /**
+   * If provided, only transform imports from these sources.
+   * The keys should match the original import source strings.
+   * If not provided, transforms all imports returned by extractImportsFromModule.
+   */
+  importsToTransform?: Map<string, Set<string>>
+
+  /**
+   * If provided, reuse this AST instead of parsing the code again.
+   * This should be the AST returned from extractImportsFromModule.
+   */
+  ast?: ParseAstResult
+
+  /**
+   * Whether to generate source maps.
+   * @default true
+   */
+  sourceMaps?: boolean
+}
+
+/**
+ * Transform import declarations to add the split-exports query parameter.
+ * Returns the transformed code or null if no changes were made.
+ *
+ * @param code - The source code to transform
+ * @param filename - The filename for source maps
+ * @param options - Optional configuration for which imports to transform
+ */
+export function transformImports(
+  code: string,
+  filename: string,
+  options: TransformImportsOptions = {},
+): GeneratorResult | null {
+  // Use provided imports or extract them (for backwards compatibility)
+  let ast: ParseAstResult
+  let imports: Map<string, Set<string>>
+
+  if (options.importsToTransform) {
+    imports = options.importsToTransform
+    // Use provided AST or parse the code
+    ast = options.ast ?? parseAst({ code })
+  } else {
+    // Extract imports and get AST in one pass
+    const result = extractImportsFromModule(code)
+    imports = result.imports
+    ast = result.ast
+  }
+
+  if (imports.size === 0) {
+    return null
+  }
+
+  // Track if we actually modified anything
+  let hasChanges = false
+
+  // Simple loop over top-level statements - imports are always at the top level
+  for (const node of ast.program.body) {
+    if (!t.isImportDeclaration(node)) {
+      continue
+    }
+
+    const source = node.source.value
+
+    // Only transform if this source is in our imports map
+    // (extractImportsFromModule already filtered out type-only, namespace, etc.)
+    const importedNames = imports.get(source)
+    if (!importedNames || importedNames.size === 0) {
+      continue
+    }
+
+    // Create new source with query string
+    node.source = t.stringLiteral(
+      appendSplitExportsQuery(source, importedNames),
+    )
+    hasChanges = true
+  }
+
+  if (!hasChanges) {
+    return null
+  }
+
+  return generateFromAst(ast, {
+    sourceMaps: options.sourceMaps ?? true,
+    sourceFileName: filename,
+    filename,
+  })
+}
+
+/**
+ * Options for transforming exports.
+ */
+export interface TransformExportsOptions {
+  /**
+   * Whether to generate source maps.
+   * @default true
+   */
+  sourceMaps?: boolean
+}
+
+/**
+ * Result of extracting all export names from a module.
+ */
+export interface GetAllExportNamesResult {
+  /** Set of all export names in the module */
+  exportNames: Set<string>
+  /** The parsed AST, can be reused by transformExports */
+  ast: ParseAstResult
+  /** True if the module has wildcard re-exports (export * from) */
+  hasWildcardReExport: boolean
+}
+
+/**
+ * Extract exported names from a declaration node.
+ */
+function getExportedNamesFromDeclaration(
+  declaration: t.Declaration,
+): Array<string> {
+  if (t.isVariableDeclaration(declaration)) {
+    return declaration.declarations
+      .filter((decl): decl is t.VariableDeclarator & { id: t.Identifier } =>
+        t.isIdentifier(decl.id),
+      )
+      .map((decl) => decl.id.name)
+  }
+
+  if (
+    (t.isFunctionDeclaration(declaration) ||
+      t.isClassDeclaration(declaration)) &&
+    declaration.id
+  ) {
+    return [declaration.id.name]
+  }
+
+  return []
+}
+
+/**
+ * Get the exported name from an export specifier.
+ */
+function getExportedName(specifier: t.ExportSpecifier): string {
+  return t.isIdentifier(specifier.exported)
+    ? specifier.exported.name
+    : specifier.exported.value
+}
+
+/**
+ * Add binding identifiers from a path to the refIdents set for dead code elimination.
+ * This ensures that local declarations created from removed exports can be eliminated.
+ */
+function addBindingIdentifiersToRefIdents(
+  path: babel.NodePath,
+  refIdents: Set<babel.NodePath<t.Identifier>>,
+): void {
+  path.traverse({
+    Identifier(identPath: babel.NodePath<t.Identifier>) {
+      // Add function/class name identifiers
+      if (
+        (identPath.parentPath.isFunctionDeclaration() ||
+          identPath.parentPath.isClassDeclaration()) &&
+        identPath.key === 'id'
+      ) {
+        refIdents.add(identPath)
+      }
+      // Add variable declarator identifiers
+      if (
+        identPath.parentPath.isVariableDeclarator() &&
+        identPath.key === 'id'
+      ) {
+        refIdents.add(identPath)
+      }
+    },
+  })
+}
+
+/**
+ * Quickly extract all export names from a module.
+ * This is used for early bailout optimization when all exports are requested.
+ * Returns the AST so it can be reused by transformExports if needed.
+ */
+export function getAllExportNames(code: string): GetAllExportNamesResult {
+  const ast = parseAst({ code })
+  const exportNames = new Set<string>()
+  let hasWildcardReExport = false
+
+  for (const node of ast.program.body) {
+    if (t.isExportNamedDeclaration(node)) {
+      // Handle: export const foo = ..., export function bar() {}, etc.
+      if (node.declaration) {
+        for (const name of getExportedNamesFromDeclaration(node.declaration)) {
+          exportNames.add(name)
+        }
+      }
+
+      // Handle: export { foo, bar as baz } and export { foo } from './other'
+      for (const specifier of node.specifiers) {
+        if (t.isExportSpecifier(specifier)) {
+          exportNames.add(getExportedName(specifier))
+        }
+      }
+    } else if (t.isExportDefaultDeclaration(node)) {
+      exportNames.add('default')
+    } else if (t.isExportAllDeclaration(node)) {
+      // We can't enumerate exports from wildcard re-exports
+      hasWildcardReExport = true
+    }
+  }
+
+  return { exportNames, ast, hasWildcardReExport }
+}
+
+/**
+ * Transform a module to only export the specified identifiers.
+ * All other exports are converted to local declarations and dead code is eliminated.
+ * Returns null if no changes were made.
+ */
+export function transformExports(
+  code: string,
+  filename: string,
+  exportsToKeep: Set<string>,
+  options: TransformExportsOptions = {},
+): GeneratorResult | null {
+  // Extract all export names and get the AST in one pass
+  // This allows early bailout if all exports are being kept
+  const {
+    exportNames: allExports,
+    ast,
+    hasWildcardReExport,
+  } = getAllExportNames(code)
+
+  // Early bailout: if we can enumerate all exports and all are being kept,
+  // skip the expensive transformation (findReferencedIdentifiers, etc.)
+  if (!hasWildcardReExport) {
+    const allKept = [...allExports].every((name) => exportsToKeep.has(name))
+    if (allKept) {
+      // All exports are being kept, no transformation needed
+      return null
+    }
+  }
+
+  // Find all referenced identifiers before transformation
+  const refIdents = findReferencedIdentifiers(ast)
+
+  // Track if we actually modified anything
+  let hasChanges = false
+
+  babel.traverse(ast, {
+    ExportNamedDeclaration(path) {
+      const node = path.node
+
+      if (node.declaration) {
+        // export const/let/var foo = ..., export function bar() {}, export class Baz {}
+        const names = getExportedNamesFromDeclaration(node.declaration)
+        const keptNames = names.filter((name) => exportsToKeep.has(name))
+
+        if (keptNames.length === names.length) {
+          // All kept, no changes needed
+          return
+        }
+
+        hasChanges = true
+
+        if (keptNames.length === 0) {
+          // None kept - convert entire declaration to local, let DCE handle unused parts
+          path.replaceWith(node.declaration)
+          // Add identifiers to refIdents so DCE can remove them if unused
+          addBindingIdentifiersToRefIdents(path, refIdents)
+        } else if (t.isVariableDeclaration(node.declaration)) {
+          // Some kept - need to split variable declarations
+          // e.g., `export const a = 1, b = 2, c = 3` with keep=[a,c]
+          // becomes `export const a = 1, c = 3` + `const b = 2`
+          const keptDeclarators: Array<t.VariableDeclarator> = []
+          const removedDeclarators: Array<t.VariableDeclarator> = []
+
+          for (const decl of node.declaration.declarations) {
+            if (t.isIdentifier(decl.id) && exportsToKeep.has(decl.id.name)) {
+              keptDeclarators.push(decl)
+            } else {
+              removedDeclarators.push(decl)
+            }
+          }
+
+          // Replace the export with just the kept declarators
+          node.declaration.declarations = keptDeclarators
+
+          // If there are removed declarators, add them as a local declaration
+          if (removedDeclarators.length > 0) {
+            const localDecl = t.variableDeclaration(
+              node.declaration.kind,
+              removedDeclarators,
+            )
+            const [insertedPath] = path.insertAfter(localDecl)
+            // Add identifiers to refIdents so DCE can remove them if unused
+            addBindingIdentifiersToRefIdents(insertedPath, refIdents)
+          }
+        } else {
+          // Unreachable for function/class: they export exactly one name,
+          // so keptNames is either length 0 (handled above) or 1 (all kept, returned early)
+          // Keep as defensive fallback
+          path.replaceWith(node.declaration)
+          // Add identifiers to refIdents so DCE can remove them if unused
+          addBindingIdentifiersToRefIdents(path, refIdents)
+        }
+      } else if (node.specifiers.length > 0) {
+        // export { foo, bar } or export { foo, bar } from './other'
+        const keptSpecifiers = node.specifiers.filter((spec) => {
+          if (t.isExportSpecifier(spec)) {
+            return exportsToKeep.has(getExportedName(spec))
+          }
+          return false
+        })
+
+        if (keptSpecifiers.length === node.specifiers.length) {
+          // All kept, no changes needed
+          return
+        }
+
+        hasChanges = true
+
+        if (keptSpecifiers.length > 0) {
+          node.specifiers = keptSpecifiers
+        } else {
+          path.remove()
+        }
+      }
+    },
+
+    ExportDefaultDeclaration(path) {
+      if (exportsToKeep.has('default')) {
+        return
+      }
+
+      hasChanges = true
+
+      const decl = path.node.declaration
+      if (
+        (t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) &&
+        decl.id
+      ) {
+        // Named: `export default function foo() {}` -> `function foo() {}`
+        // Keep as local declaration so DCE can decide if it's referenced elsewhere
+        path.replaceWith(decl)
+        // Add identifiers to refIdents so DCE can remove them if unused
+        addBindingIdentifiersToRefIdents(path, refIdents)
+      } else {
+        // Anonymous function/class or expression - just remove
+        path.remove()
+      }
+    },
+
+    // Note: ExportAllDeclaration (export * from './foo') is intentionally not handled
+    // We leave these as-is and let the bundler tree-shake them
+  })
+
+  // hasChanges may remain false if:
+  // - Module only has wildcard re-exports (export * from) which we preserve
+  // - All named exports are in exportsToKeep (but we didn't bail out due to wildcard)
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+  if (!hasChanges) {
+    return null
+  }
+
+  // Run dead code elimination
+  deadCodeElimination(ast, refIdents)
+
+  return generateFromAst(ast, {
+    sourceMaps: options.sourceMaps ?? true,
+    sourceFileName: filename,
+    filename,
+  })
+}
diff --git a/packages/start-plugin-core/src/split-exports-plugin/plugin-utils.ts b/packages/start-plugin-core/src/split-exports-plugin/plugin-utils.ts
new file mode 100644
index 0000000000..2e0d9ecf06
--- /dev/null
+++ b/packages/start-plugin-core/src/split-exports-plugin/plugin-utils.ts
@@ -0,0 +1,104 @@
+import { normalizePath } from 'vite'
+import { stripQueryString } from '../plugin-utils'
+
+export const debug =
+  process.env.TSR_VITE_DEBUG &&
+  ['true', 'split-exports'].includes(process.env.TSR_VITE_DEBUG)
+
+/**
+ * File extensions that can be parsed as JavaScript/TypeScript.
+ * These are the only files we should attempt to transform.
+ */
+const PARSEABLE_EXTENSIONS = new Set([
+  '.js',
+  '.jsx',
+  '.ts',
+  '.tsx',
+  '.mjs',
+  '.mts',
+  '.cjs',
+  '.cts',
+])
+
+/**
+ * Query parameters that indicate the module is being processed by
+ * the directive functions plugin (server function extraction).
+ * We should NOT transform exports in these modules because they
+ * need to preserve the handler exports.
+ */
+const DIRECTIVE_QUERY_PATTERN = /[?&]tsr-directive-/
+
+/**
+ * Check if a module ID has a directive query parameter.
+ * These modules are being processed for server function extraction
+ * and should not have their exports transformed.
+ */
+export function hasDirectiveQuery(id: string): boolean {
+  return DIRECTIVE_QUERY_PATTERN.test(id)
+}
+
+/**
+ * Check if a file path has an extension that can be parsed as JS/TS.
+ * Returns false for CSS, images, fonts, and other asset files.
+ */
+export function isParseableFile(id: string): boolean {
+  // Strip query string first
+  const cleanId = stripQueryString(id)
+
+  // Find the extension
+  const lastDot = cleanId.lastIndexOf('.')
+  if (lastDot === -1) {
+    // No extension - might be a virtual module, let it through
+    return true
+  }
+
+  const ext = cleanId.slice(lastDot).toLowerCase()
+  return PARSEABLE_EXTENSIONS.has(ext)
+}
+
+/**
+ * Check if an ID should be excluded from transformation.
+ * If srcDirectory is provided, only files inside it are processed.
+ */
+export function shouldExclude(
+  id: string,
+  srcDirectory: string | undefined,
+  exclude: Array<string | RegExp> | undefined,
+): boolean {
+  // Normalize and strip query string for consistent path comparison
+  const cleanId = normalizePath(stripQueryString(id))
+
+  // Always exclude node_modules (safety net)
+  if (cleanId.includes('/node_modules/')) {
+    return true
+  }
+
+  // If srcDirectory is provided, only include files inside it
+  if (srcDirectory) {
+    const normalizedSrcDir = normalizePath(srcDirectory)
+    if (!cleanId.startsWith(normalizedSrcDir)) {
+      if (debug) {
+        console.info(
+          '[split-exports] Skipping (outside srcDirectory):',
+          cleanId,
+        )
+      }
+      return true
+    }
+  }
+
+  // Check custom exclusion patterns
+  if (exclude && exclude.length > 0) {
+    for (const pattern of exclude) {
+      if (typeof pattern === 'string') {
+        if (cleanId.includes(pattern)) {
+          return true
+        }
+      } else if (pattern.test(cleanId)) {
+        return true
+      }
+    }
+  }
+
+  return false
+}
diff --git a/packages/start-plugin-core/src/split-exports-plugin/plugin.ts b/packages/start-plugin-core/src/split-exports-plugin/plugin.ts
new file mode 100644
index 0000000000..ef3f7722f2
--- /dev/null
+++ b/packages/start-plugin-core/src/split-exports-plugin/plugin.ts
@@ -0,0 +1,474 @@
+import { logDiff } from '@tanstack/router-utils'
+import { normalizePath } from 'vite'
+import { TRANSFORM_ID_REGEX } from '../constants'
+import type { ModuleLoaderApi } from '../module-loader-plugin/plugin'
+import { findModuleLoaderApi, stripQueryString } from '../plugin-utils'
+import {
+  extractImportsFromModule,
+  hasClassExports,
+  transformExports,
+  transformImports,
+} from './compiler'
+import {
+  debug,
+  hasDirectiveQuery,
+  isParseableFile,
+  shouldExclude,
+} from './plugin-utils'
+import {
+  SPLIT_EXPORTS_QUERY_KEY,
+  extractSplitExportsQuery,
+  hasSplitExportsQuery,
+  parseSplitExportsQuery,
+  removeSplitExportsQuery,
+} from './query-utils'
+import type { GetConfigFn } from '../plugin'
+import type { Plugin, PluginOption, ResolvedConfig } from 'vite'
+
+/**
+ * Determine if sourcemaps should be generated based on Vite config.
+ * Returns true for 'inline', 'hidden', true, or any truthy string value.
+ */
+function shouldGenerateSourceMaps(config: ResolvedConfig | undefined): boolean {
+  if (!config) return true // Default to generating sourcemaps if no config
+  const sourcemap = config.build.sourcemap
+  // sourcemap can be boolean, 'inline', 'hidden', or undefined
+  return !!sourcemap
+}
+
+/**
+ * Options for the split-exports plugin.
+ */
+export interface SplitExportsPluginOptions {
+  /**
+   * Enable or disable the plugin.
+   * @default true
+   */
+  enabled?: boolean
+
+  /**
+   * Paths to exclude from transformation (glob patterns).
+   * node_modules is always excluded.
+   */
+  exclude?: Array<string | RegExp>
+
+  /**
+   * Function to get the resolved Start config.
+   * When provided, only files inside srcDirectory will be transformed.
+   */
+  getConfig?: GetConfigFn
+}
+
+/**
+ * Plugin 1: Rewrite imports to add split-exports query parameters.
+ * This runs early (enforce: 'pre') to add query strings before other plugins process the imports.
+ */
+function importRewriterPlugin(options: SplitExportsPluginOptions): Plugin {
+  let srcDirectory: string | undefined
+  let sourceMaps = true
+  let loaderApi: ModuleLoaderApi | undefined
+
+  // Cache for class export detection per resolved path
+  // true = has class exports (skip), false = no class exports (can optimize)
+  const classExportCache = new Map<string, boolean>()
+
+  return {
+    name: 'tanstack-split-exports:rewrite-imports',
+    enforce: 'pre',
+
+    configResolved(config) {
+      // Resolve srcDirectory once when config is ready
+      if (options.getConfig) {
+        const { resolvedStartConfig } = options.getConfig()
+        srcDirectory = resolvedStartConfig.srcDirectory
+      }
+      // Capture sourcemap setting from Vite config
+      sourceMaps = shouldGenerateSourceMaps(config)
+    },
+
+    transform: {
+      filter: {
+        id: TRANSFORM_ID_REGEX,
+      },
+      async handler(code, id) {
+        // Skip excluded paths (including files outside srcDirectory)
+        // Note: We intentionally do NOT skip files with tss-split-exports query here.
+        // A file like `foo.ts?tss-split-exports=Route` may still have imports
+        // that need to be rewritten (e.g., `import { bar } from './bar'`).
+        // The extractImportsFromModule function already skips imports that have
+        // our query parameter, preventing infinite recursion.
+        if (shouldExclude(id, srcDirectory, options.exclude)) {
+          return null
+        }
+
+        // Quick check: skip files that don't contain import statements
+        if (!code.includes('import')) {
+          return null
+        }
+
+        // Extract all potential imports (including aliased ones like ~/ and @/)
+        // Also get the AST to reuse in transformImports (avoids double parsing)
+        const { imports: allImports, ast } = extractImportsFromModule(code)
+
+        if (allImports.size === 0) {
+          return null
+        }
+
+        // Get the module loader API (looked up once, then cached)
+        if (!loaderApi) {
+          loaderApi = findModuleLoaderApi(this.environment.plugins)
+        }
+
+        // Resolve all imports in parallel for better performance
+        const importEntries = Array.from(allImports.entries())
+        const resolutions = await Promise.all(
+          importEntries.map(async ([source]) => {
+            const resolved = await this.resolve(source, id, { skipSelf: true })
+            return { source, resolved }
+          }),
+        )
+
+        // Build a map of source -> resolved path for filtering
+        const resolvedPaths = new Map<string, string>()
+        for (const { source, resolved } of resolutions) {
+          if (resolved?.id) {
+            resolvedPaths.set(
+              source,
+              normalizePath(stripQueryString(resolved.id)),
+            )
+          }
+        }
+
+        // Filter imports based on resolved paths
+        const importsToTransform = new Map<string, Set<string>>()
+        const normalizedSrcDir = srcDirectory
+          ? normalizePath(srcDirectory)
+          : undefined
+
+        for (const [source, names] of allImports) {
+          const resolvedPath = resolvedPaths.get(source)
+          if (!resolvedPath) {
+            continue
+          }
+
+          // Skip non-parseable files (CSS, images, etc.)
+          if (!isParseableFile(resolvedPath)) {
+            if (debug) {
+              console.info(
+                '[split-exports] Skipping import (non-parseable file):',
+                source,
+                '->',
+                resolvedPath,
+              )
+            }
+            continue
+          }
+
+          // Skip node_modules
+          if (resolvedPath.includes('/node_modules/')) {
+            continue
+          }
+
+          // If srcDirectory is set, only include files inside it
+          if (normalizedSrcDir && !resolvedPath.startsWith(normalizedSrcDir)) {
+            if (debug) {
+              console.info(
+                '[split-exports] Skipping import (outside srcDirectory):',
+                source,
+                '->',
+                resolvedPath,
+              )
+            }
+            continue
+          }
+
+          // Check if target module exports classes (skip if so to preserve class identity)
+          let hasClasses = classExportCache.get(resolvedPath)
+          if (hasClasses === undefined) {
+            try {
+              // In build mode, we need to pass ctx.load for loading modules
+              const ctxLoad =
+                this.environment.mode === 'build'
+                  ? this.load.bind(this)
+                  : undefined
+              const targetCode = await loaderApi.loadModuleCode(
+                this.environment,
+                resolvedPath,
+                ctxLoad,
+              )
+              hasClasses = hasClassExports(targetCode)
+              classExportCache.set(resolvedPath, hasClasses)
+            } catch {
+              // If we can't load the module, assume no classes (safe fallback)
+              hasClasses = false
+              classExportCache.set(resolvedPath, hasClasses)
+            }
+          }
+
+          if (hasClasses) {
+            if (debug) {
+              console.info(
+                '[split-exports] Skipping import (module exports class):',
+                source,
+                '->',
+                resolvedPath,
+              )
+            }
+            continue
+          }
+
+          importsToTransform.set(source, names)
+          if (debug) {
+            console.info(
+              '[split-exports] Including import:',
+              source,
+              '->',
+              resolvedPath,
+            )
+          }
+        }
+
+        if (importsToTransform.size === 0) {
+          return null
+        }
+
+        const result = transformImports(code, id, {
+          importsToTransform,
+          ast, // Reuse the AST from extractImportsFromModule
+          sourceMaps, // Skip sourcemap generation if disabled in Vite config
+        })
+
+        if (result && debug) {
+          console.info(
+            `[split-exports env: ${this.environment.name}] Rewrote imports in:`,
+            id,
+          )
+          logDiff(code, result.code)
+          console.log('Output:\n', result.code + '\n\n')
+        }
+
+        return result
+      },
+    },
+
+    // Invalidate class export cache on HMR
+    hotUpdate(ctx) {
+      for (const mod of ctx.modules) {
+        if (mod.id) {
+          classExportCache.delete(mod.id)
+        }
+      }
+    },
+  }
+}
+
+/**
+ * Plugin 2: Resolve split-exports query IDs.
+ * When we see an import like './foo?tss-split-exports=bar', we resolve the base path
+ * and re-attach our query parameter.
+ */
+function resolverPlugin(): Plugin {
+  return {
+    name: 'tanstack-split-exports:resolve',
+
+    resolveId: {
+      filter: {
+        id: new RegExp(`[?&]${SPLIT_EXPORTS_QUERY_KEY}=`),
+      },
+      async handler(id, importer, resolveOptions) {
+        // Extract our query and the clean path
+        const { cleanId, exportNames } = extractSplitExportsQuery(id)
+
+        // Resolve the clean path using Vite's normal resolution
+        const resolved = await this.resolve(cleanId, importer, {
+          ...resolveOptions,
+          skipSelf: true,
+        })
+
+        if (!resolved) {
+          return null
+        }
+
+        // Re-attach our query to the resolved path
+        // Check if the resolved ID already has query params
+        const hasQuery = resolved.id.includes('?')
+        const separator = hasQuery ? '&' : '?'
+        // URL-encode individual names to handle $ and unicode characters in identifiers
+        const sortedNames = Array.from(exportNames)
+          .sort()
+          .map(encodeURIComponent)
+          .join(',')
+
+        return {
+          ...resolved,
+          id: `${resolved.id}${separator}${SPLIT_EXPORTS_QUERY_KEY}=${sortedNames}`,
+        }
+      },
+    },
+  }
+}
+
+/**
+ * Plugin 3: Transform modules with split-exports query to only export requested symbols.
+ * This runs after other transforms (like TypeScript compilation) have processed the code.
+ */
+function exportTransformerPlugin(): Plugin {
+  let sourceMaps = true
+
+  return {
+    name: 'tanstack-split-exports:transform-exports',
+    enforce: 'pre',
+
+    configResolved(config) {
+      // Capture sourcemap setting from Vite config
+      sourceMaps = shouldGenerateSourceMaps(config)
+    },
+
+    transform: {
+      filter: {
+        id: new RegExp(`[?&]${SPLIT_EXPORTS_QUERY_KEY}=`),
+      },
+      handler(code, id) {
+        // Safety check: skip non-parseable files (CSS, images, etc.)
+        // This shouldn't happen if importRewriterPlugin is working correctly,
+        // but we check here as a safety net
+        if (!isParseableFile(id)) {
+          return null
+        }
+
+        // Skip modules that are being processed by the directive functions plugin
+        // (server function extraction). These modules need to preserve their
+        // handler exports which may not be in the split-exports list.
+        if (hasDirectiveQuery(id)) {
+          if (debug) {
+            console.info('[split-exports] Skipping (has directive query):', id)
+          }
+          return null
+        }
+
+        const exportsToKeep = parseSplitExportsQuery(id)
+        if (!exportsToKeep || exportsToKeep.size === 0) {
+          return null
+        }
+
+        const cleanId = removeSplitExportsQuery(id)
+
+        if (debug) {
+          console.info(
+            `[split-exports env: ${this.environment.name}] Transforming exports in:`,
+            id,
+            'keeping:',
+            Array.from(exportsToKeep),
+          )
+        }
+
+        const result = transformExports(code, cleanId, exportsToKeep, {
+          sourceMaps, // Skip sourcemap generation if disabled in Vite config
+        })
+
+        if (!result) {
+          return null
+        }
+
+        if (debug) {
+          logDiff(code, result.code)
+          console.log('Output:\n', result.code + '\n\n')
+        }
+
+        return result
+      },
+    },
+  }
+}
+
+/**
+ * Plugin 4: Handle HMR invalidation for split-exports modules.
+ * When a source file changes, we need to invalidate all the split-exports variants of it.
+ */
+function hmrPlugin(): Plugin {
+  return {
+    name: 'tanstack-split-exports:hmr',
+    apply: 'serve',
+
+    hotUpdate(ctx) {
+      const affectedModules: Set<(typeof ctx.modules)[number]> = new Set()
+
+      for (const mod of ctx.modules) {
+        if (!mod.id) continue
+
+        const modIdWithoutQuery = removeSplitExportsQuery(mod.id)
+
+        // Find all modules in the graph that are split-exports variants of this file
+        for (const [id, module] of this.environment.moduleGraph.idToModuleMap) {
+          if (!hasSplitExportsQuery(id)) {
+            continue
+          }
+
+          const cleanId = removeSplitExportsQuery(id)
+
+          // Check if this split-exports module is derived from the changed file
+          if (cleanId === modIdWithoutQuery || cleanId === mod.id) {
+            // Invalidate this split-exports variant
+            affectedModules.add(module)
+
+            // Also mark importers for update so they can re-evaluate
+            for (const importer of module.importers) {
+              affectedModules.add(importer)
+            }
+          }
+        }
+      }
+
+      // Add affected modules to the update
+      if (affectedModules.size > 0) {
+        return [...ctx.modules, ...affectedModules]
+      }
+
+      return undefined
+    },
+  }
+}
+
+/**
+ * Creates the split-exports Vite plugin.
+ *
+ * This plugin automatically optimizes imports by:
+ * 1. Analyzing which exports are actually imported from each module
+ * 2. Rewriting imports to include a query string specifying needed exports
+ * 3. Transforming the imported modules to only export what's needed
+ * 4. Running dead code elimination to remove unused code
+ *
+ * This prevents server-only code from leaking into client bundles when
+ * a module exports both isomorphic and server-only code.
+ *
+ * @example
+ * ```ts
+ * // vite.config.ts
+ * import { splitExportsPlugin } from '@tanstack/start-plugin-core'
+ *
+ * export default {
+ *   plugins: [
+ *     splitExportsPlugin({
+ *       enabled: true,
+ *       debug: false,
+ *     }),
+ *   ],
+ * }
+ * ```
+ */
+export function splitExportsPlugin(
+  options: SplitExportsPluginOptions = {},
+): PluginOption {
+  const { enabled = true } = options
+
+  if (!enabled) {
+    return []
+  }
+
+  return [
+    importRewriterPlugin(options),
+    resolverPlugin(),
+    exportTransformerPlugin(),
+    hmrPlugin(),
+  ]
+}
diff --git a/packages/start-plugin-core/src/split-exports-plugin/query-utils.ts b/packages/start-plugin-core/src/split-exports-plugin/query-utils.ts
new file mode 100644
index 0000000000..827019748a
--- /dev/null
+++ b/packages/start-plugin-core/src/split-exports-plugin/query-utils.ts
@@ -0,0 +1,103 @@
+export const SPLIT_EXPORTS_QUERY_KEY = 'tss-split-exports'
+
+/**
+ * Split a module ID into path and query string parts.
+ * Uses a simple split since module IDs may not be valid URLs.
+ */
+function splitIdParts(id: string): { path: string; query: string } {
+  const queryIndex = id.indexOf('?')
+  if (queryIndex === -1) {
+    return { path: id, query: '' }
+  }
+  return {
+    path: id.slice(0, queryIndex),
+    query: id.slice(queryIndex + 1),
+  }
+}
+
+/**
+ * Check if an ID has the split-exports query parameter.
+ * Uses simple string matching for performance.
+ */
+export function hasSplitExportsQuery(id: string): boolean {
+  return (
+    id.includes(`?${SPLIT_EXPORTS_QUERY_KEY}=`) ||
+    id.includes(`&${SPLIT_EXPORTS_QUERY_KEY}=`)
+  )
+}
+
+/**
+ * Parse the split-exports query parameter from a module ID.
+ * Returns the set of exported names that should be kept, or null if no query is present.
+ * Note: We manually extract the raw value to avoid URLSearchParams auto-decoding,
+ * which allows us to correctly split on comma before decoding individual names.
+ */
+export function parseSplitExportsQuery(id: string): Set<string> | null {
+  const { query } = splitIdParts(id)
+  if (!query) return null
+
+  // Manually find the parameter value to avoid URLSearchParams auto-decoding
+  const prefix = `${SPLIT_EXPORTS_QUERY_KEY}=`
+  const params = query.split('&')
+  const param = params.find((p) => p.startsWith(prefix))
+  if (!param) return null
+
+  const rawValue = param.slice(prefix.length)
+  if (!rawValue) return null
+
+  // Split on comma first (our separator), then decode each name
+  return new Set(rawValue.split(',').filter(Boolean).map(decodeURIComponent))
+}
+
+/**
+ * Remove the split-exports query parameter from a module ID.
+ * Preserves other query parameters.
+ */
+export function removeSplitExportsQuery(id: string): string {
+  const { path, query } = splitIdParts(id)
+  if (!query) return id
+
+  const params = new URLSearchParams(query)
+  if (!params.has(SPLIT_EXPORTS_QUERY_KEY)) return id
+
+  params.delete(SPLIT_EXPORTS_QUERY_KEY)
+  const remainingQuery = params.toString()
+  return remainingQuery ? `${path}?${remainingQuery}` : path
+}
+
+/**
+ * Append the split-exports query parameter to a source path.
+ * Handles existing query strings correctly.
+ * Export names are URL-encoded to handle $ and unicode characters in identifiers.
+ */
+export function appendSplitExportsQuery(
+  source: string,
+  exportNames: Set<string>,
+): string {
+  if (exportNames.size === 0) return source
+
+  const { path, query } = splitIdParts(source)
+
+  // Sort names for consistent, cache-friendly query strings
+  // URL-encode individual names to handle $ and unicode characters in identifiers
+  const sortedNames = Array.from(exportNames).sort().map(encodeURIComponent)
+  const newParam = `${SPLIT_EXPORTS_QUERY_KEY}=${sortedNames.join(',')}`
+
+  if (!query) {
+    return `${path}?${newParam}`
+  }
+
+  return `${path}?${query}&${newParam}`
+}
+
+/**
+ * Extract the split-exports query from an ID and return both the clean ID and export names.
+ */
+export function extractSplitExportsQuery(id: string): {
+  cleanId: string
+  exportNames: Set<string>
+} {
+  const exportNames = parseSplitExportsQuery(id) || new Set()
+  const cleanId = removeSplitExportsQuery(id)
+  return { cleanId, exportNames }
+}
diff --git a/packages/start-plugin-core/tests/plugin-utils.test.ts b/packages/start-plugin-core/tests/plugin-utils.test.ts
new file mode 100644
index 0000000000..21437f9858
--- /dev/null
+++ b/packages/start-plugin-core/tests/plugin-utils.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, test } from 'vitest'
+import { stripQueryString } from '../src/plugin-utils'
+
+describe('stripQueryString', () => {
+  test('strips query string from path', () => {
+    expect(stripQueryString('/path/to/file.ts?v=123')).toBe('/path/to/file.ts')
+  })
+
+  test('returns path unchanged when no query string', () => {
+    expect(stripQueryString('/path/to/file.ts')).toBe('/path/to/file.ts')
+  })
+
+  test('handles multiple query parameters', () => {
+    expect(stripQueryString('/path/to/file.ts?v=123&other=abc')).toBe(
+      '/path/to/file.ts',
+    )
+  })
+})
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/defaultExport.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/defaultExport.ts
new file mode 100644
index 0000000000..ea832b9109
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/defaultExport.ts
@@ -0,0 +1,5 @@
+// Test file: Default export
+const helper = () => 'helper';
+export default function main() {
+  return helper();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/functionExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/functionExports.ts
new file mode 100644
index 0000000000..5fde2afb6b
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/functionExports.ts
@@ -0,0 +1,4 @@
+// Test file: Multiple function exports
+export function myFunction() {
+  return 'function';
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/multipleDeclarators.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/multipleDeclarators.ts
new file mode 100644
index 0000000000..de781e6b09
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/multipleDeclarators.ts
@@ -0,0 +1,5 @@
+// Test file: Export with declaration containing multiple declarators
+export const a = 1;
+export function processA() {
+  return a * 2;
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/namedExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/namedExports.ts
new file mode 100644
index 0000000000..b0160485dd
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/namedExports.ts
@@ -0,0 +1,3 @@
+// Test file: Multiple named exports - some should be removed
+export const foo = () => 'foo';
+export const bar = () => 'bar';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/reExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/reExports.ts
new file mode 100644
index 0000000000..a51df7f7cc
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/reExports.ts
@@ -0,0 +1,3 @@
+// Test file: Re-exports from other modules
+export { foo } from './source';
+export { helper as renamedHelper } from './helpers';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/serverOnlyCode.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/serverOnlyCode.ts
new file mode 100644
index 0000000000..4c046f08b4
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/serverOnlyCode.ts
@@ -0,0 +1,13 @@
+// Test file: Complex module with server-only code (real-world scenario)
+// server-only
+
+// This is isomorphic - used on both client and server
+export const formatUser = (user: {
+  name: string;
+}) => {
+  return user.name.toUpperCase();
+};
+
+// This is server-only - should be eliminated when only formatUser is imported
+
+// This is also server-only
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/starReExport.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/starReExport.ts
new file mode 100644
index 0000000000..b002be3479
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/exports/starReExport.ts
@@ -0,0 +1,3 @@
+// Test file: Star re-exports should be left as-is
+export * from './source';
+export { foo } from './other';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/defaultImport.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/defaultImport.ts
new file mode 100644
index 0000000000..5db85f20bc
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/defaultImport.ts
@@ -0,0 +1,6 @@
+// Test file: Default import from relative module
+import utils from "./utils?tss-split-exports=default";
+import config from "../config?tss-split-exports=default";
+export function main() {
+  return utils.process(config);
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/existingQuery.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/existingQuery.ts
new file mode 100644
index 0000000000..a32a8bdb99
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/existingQuery.ts
@@ -0,0 +1,6 @@
+// Test file: Imports with existing query string
+import { foo } from "./utils?v=123&tss-split-exports=foo";
+import { bar } from "./helpers?tss-split-exports=bar";
+export function main() {
+  return foo() + bar();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/externalImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/externalImports.ts
new file mode 100644
index 0000000000..16c3638163
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/externalImports.ts
@@ -0,0 +1,7 @@
+// Test file: External (node_modules) imports should be skipped
+import { useState } from 'react';
+import { foo } from "./utils?tss-split-exports=foo";
+export function Component() {
+  const [state] = useState(foo());
+  return state;
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedImports.ts
new file mode 100644
index 0000000000..3599157901
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedImports.ts
@@ -0,0 +1,5 @@
+// Test file: Mixed default and named imports
+import utils, { foo, bar } from "./utils?tss-split-exports=bar,default,foo";
+export function main() {
+  return utils.process() + foo() + bar();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedTypeValueImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedTypeValueImports.ts
new file mode 100644
index 0000000000..7007d8d280
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/mixedTypeValueImports.ts
@@ -0,0 +1,6 @@
+// Test file: Mixed type and value imports in same declaration
+import { type UserType, type Config, processUser, formatData } from "./utils?tss-split-exports=formatData,processUser";
+import { type HelperType, helper } from "./helpers?tss-split-exports=helper";
+export function main(user: UserType, config: Config) {
+  return processUser(user) + formatData(config) + helper();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/namedImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/namedImports.ts
new file mode 100644
index 0000000000..f000121742
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/namedImports.ts
@@ -0,0 +1,6 @@
+// Test file: Named imports from relative module
+import { foo, bar } from "./utils?tss-split-exports=bar,foo";
+import { helper } from "../shared/helpers?tss-split-exports=helper";
+export function main() {
+  return foo() + bar() + helper();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/typeOnlyImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/typeOnlyImports.ts
new file mode 100644
index 0000000000..116476436a
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/snapshots/imports/typeOnlyImports.ts
@@ -0,0 +1,6 @@
+// Test file: Type-only imports should be skipped
+import type { UserType } from './types';
+import { processUser } from "./utils?tss-split-exports=processUser";
+export function main(user: UserType) {
+  return processUser(user);
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/split-exports.test.ts b/packages/start-plugin-core/tests/split-exports-plugin/split-exports.test.ts
new file mode 100644
index 0000000000..5ed83d655b
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/split-exports.test.ts
@@ -0,0 +1,1168 @@
+import { readFile, readdir } from 'node:fs/promises'
+import path from 'node:path'
+import { describe, expect, test, vi } from 'vitest'
+
+import {
+  extractImportsFromModule,
+  getAllExportNames,
+  hasClassExports,
+  isBareModuleSpecifier,
+  transformExports,
+  transformImports,
+} from '../../src/split-exports-plugin/compiler'
+
+import {
+  isParseableFile,
+  shouldExclude,
+} from '../../src/split-exports-plugin/plugin-utils'
+import {
+  appendSplitExportsQuery,
+  extractSplitExportsQuery,
+  hasSplitExportsQuery,
+  parseSplitExportsQuery,
+  removeSplitExportsQuery,
+  SPLIT_EXPORTS_QUERY_KEY,
+} from '../../src/split-exports-plugin/query-utils'
+
+describe('split-exports-plugin compiler', () => {
+  describe('query string utilities', () => {
+    describe('hasSplitExportsQuery', () => {
+      test('returns true for query with ?', () => {
+        expect(
+          hasSplitExportsQuery(`./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar`),
+        ).toBe(true)
+      })
+
+      test('returns true for query with &', () => {
+        expect(
+          hasSplitExportsQuery(`./foo?v=1&${SPLIT_EXPORTS_QUERY_KEY}=bar`),
+        ).toBe(true)
+      })
+
+      test('returns false when no query', () => {
+        expect(hasSplitExportsQuery('./foo')).toBe(false)
+      })
+
+      test('returns false for different query', () => {
+        expect(hasSplitExportsQuery('./foo?other=bar')).toBe(false)
+      })
+    })
+
+    describe('parseSplitExportsQuery', () => {
+      test('parses single export', () => {
+        const result = parseSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar`,
+        )
+        expect(result).toEqual(new Set(['bar']))
+      })
+
+      test('parses multiple exports', () => {
+        const result = parseSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar,baz,qux`,
+        )
+        expect(result).toEqual(new Set(['bar', 'baz', 'qux']))
+      })
+
+      test('parses with existing query params', () => {
+        const result = parseSplitExportsQuery(
+          `./foo?v=1&${SPLIT_EXPORTS_QUERY_KEY}=bar,baz`,
+        )
+        expect(result).toEqual(new Set(['bar', 'baz']))
+      })
+
+      test('returns null when no query', () => {
+        expect(parseSplitExportsQuery('./foo')).toBeNull()
+      })
+
+      test('handles default export', () => {
+        const result = parseSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=default,bar`,
+        )
+        expect(result).toEqual(new Set(['default', 'bar']))
+      })
+
+      test('decodes URL-encoded characters in export names', () => {
+        // Round-trip: encoded names should be decoded back to original
+        // $ and unicode characters are valid in JS identifiers but get URL-encoded
+        const result = parseSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=%24helper,normal,%CE%B1%CE%B2%CE%B3`,
+        )
+        expect(result).toEqual(new Set(['normal', '$helper', 'αβγ']))
+      })
+    })
+
+    describe('removeSplitExportsQuery', () => {
+      test('removes query when it is the only param', () => {
+        const result = removeSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar`,
+        )
+        expect(result).toBe('./foo')
+      })
+
+      test('removes query and preserves other params (our query first)', () => {
+        const result = removeSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar&v=1`,
+        )
+        expect(result).toBe('./foo?v=1')
+      })
+
+      test('removes query and preserves other params (our query last)', () => {
+        const result = removeSplitExportsQuery(
+          `./foo?v=1&${SPLIT_EXPORTS_QUERY_KEY}=bar`,
+        )
+        expect(result).toBe('./foo?v=1')
+      })
+
+      test('removes query and preserves other params (our query middle)', () => {
+        const result = removeSplitExportsQuery(
+          `./foo?v=1&${SPLIT_EXPORTS_QUERY_KEY}=bar&x=2`,
+        )
+        expect(result).toBe('./foo?v=1&x=2')
+      })
+
+      test('returns unchanged when no query', () => {
+        expect(removeSplitExportsQuery('./foo')).toBe('./foo')
+      })
+    })
+
+    describe('appendSplitExportsQuery', () => {
+      test('appends with ? when no existing query', () => {
+        const result = appendSplitExportsQuery('./foo', new Set(['bar', 'baz']))
+        expect(result).toBe(`./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar,baz`)
+      })
+
+      test('appends with & when query exists', () => {
+        const result = appendSplitExportsQuery(
+          './foo?v=1',
+          new Set(['bar', 'baz']),
+        )
+        expect(result).toBe(`./foo?v=1&${SPLIT_EXPORTS_QUERY_KEY}=bar,baz`)
+      })
+
+      test('sorts export names alphabetically', () => {
+        const result = appendSplitExportsQuery(
+          './foo',
+          new Set(['zeta', 'alpha', 'beta']),
+        )
+        expect(result).toBe(`./foo?${SPLIT_EXPORTS_QUERY_KEY}=alpha,beta,zeta`)
+      })
+
+      test('URL-encodes special characters in export names', () => {
+        // $ and unicode characters are valid in JS identifiers but get URL-encoded
+        const result = appendSplitExportsQuery(
+          './foo',
+          new Set(['normal', '$helper', 'αβγ']),
+        )
+        expect(result).toBe(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=%24helper,normal,%CE%B1%CE%B2%CE%B3`,
+        )
+      })
+
+      test('returns unchanged when no exports', () => {
+        expect(appendSplitExportsQuery('./foo', new Set())).toBe('./foo')
+      })
+    })
+
+    describe('extractSplitExportsQuery', () => {
+      test('extracts clean ID and export names', () => {
+        const result = extractSplitExportsQuery(
+          `./foo?${SPLIT_EXPORTS_QUERY_KEY}=bar,baz`,
+        )
+        expect(result.cleanId).toBe('./foo')
+        expect(result.exportNames).toEqual(new Set(['bar', 'baz']))
+      })
+
+      test('handles no query', () => {
+        const result = extractSplitExportsQuery('./foo')
+        expect(result.cleanId).toBe('./foo')
+        expect(result.exportNames).toEqual(new Set())
+      })
+    })
+  })
+
+  describe('isBareModuleSpecifier', () => {
+    describe('returns true for bare modules (npm packages)', () => {
+      test('simple package name', () => {
+        expect(isBareModuleSpecifier('lodash')).toBe(true)
+      })
+
+      test('package with subpath', () => {
+        expect(isBareModuleSpecifier('lodash/get')).toBe(true)
+      })
+
+      test('scoped package', () => {
+        expect(isBareModuleSpecifier('@tanstack/react-router')).toBe(true)
+      })
+
+      test('scoped package with subpath', () => {
+        expect(isBareModuleSpecifier('@tanstack/react-router/client')).toBe(
+          true,
+        )
+      })
+
+      test('scoped package with hyphen in scope', () => {
+        expect(isBareModuleSpecifier('@some-scope/package')).toBe(true)
+      })
+
+      test('scoped package with numbers in scope', () => {
+        expect(isBareModuleSpecifier('@scope123/package')).toBe(true)
+      })
+
+      test('react-dom/client', () => {
+        expect(isBareModuleSpecifier('react-dom/client')).toBe(true)
+      })
+    })
+
+    describe('returns false for relative/absolute paths', () => {
+      test('relative current directory', () => {
+        expect(isBareModuleSpecifier('./foo')).toBe(false)
+      })
+
+      test('relative parent directory', () => {
+        expect(isBareModuleSpecifier('../bar')).toBe(false)
+      })
+
+      test('absolute path', () => {
+        expect(isBareModuleSpecifier('/absolute/path')).toBe(false)
+      })
+    })
+
+    describe('returns false for path aliases', () => {
+      test('tilde alias', () => {
+        expect(isBareModuleSpecifier('~/utils')).toBe(false)
+      })
+
+      test('tilde alias with deep path', () => {
+        expect(isBareModuleSpecifier('~/utils/seo')).toBe(false)
+      })
+
+      test('@ alias (empty scope)', () => {
+        expect(isBareModuleSpecifier('@/components')).toBe(false)
+      })
+
+      test('@ alias with deep path', () => {
+        expect(isBareModuleSpecifier('@/components/Button')).toBe(false)
+      })
+
+      test('hash alias', () => {
+        expect(isBareModuleSpecifier('#/internal')).toBe(false)
+      })
+
+      test('hash alias with deep path', () => {
+        expect(isBareModuleSpecifier('#/internal/utils')).toBe(false)
+      })
+
+      test('PascalCase alias (not valid npm scope)', () => {
+        expect(isBareModuleSpecifier('@Components/Button')).toBe(false)
+      })
+
+      test('uppercase alias', () => {
+        expect(isBareModuleSpecifier('@UI/Modal')).toBe(false)
+      })
+
+      test('bare @ without slash', () => {
+        expect(isBareModuleSpecifier('@')).toBe(false)
+      })
+    })
+  })
+
+  describe('hasClassExports', () => {
+    describe('returns true for class exports', () => {
+      test('export class Foo {}', () => {
+        expect(hasClassExports('export class Foo {}')).toBe(true)
+      })
+
+      test('export default class Foo {}', () => {
+        expect(hasClassExports('export default class Foo {}')).toBe(true)
+      })
+
+      test('export default class {} (anonymous)', () => {
+        expect(hasClassExports('export default class {}')).toBe(true)
+      })
+
+      test('class Foo {}; export { Foo }', () => {
+        expect(hasClassExports('class Foo {}; export { Foo }')).toBe(true)
+      })
+
+      test('class Foo {}; export { Foo as Bar }', () => {
+        expect(hasClassExports('class Foo {}; export { Foo as Bar }')).toBe(
+          true,
+        )
+      })
+
+      test('export const Foo = class {}', () => {
+        expect(hasClassExports('export const Foo = class {}')).toBe(true)
+      })
+
+      test('export const Bar = class BarClass {}', () => {
+        expect(hasClassExports('export const Bar = class BarClass {}')).toBe(
+          true,
+        )
+      })
+
+      test('const Foo = class {}; export { Foo }', () => {
+        expect(hasClassExports('const Foo = class {}; export { Foo }')).toBe(
+          true,
+        )
+      })
+
+      test('export const A = 1, B = class {}', () => {
+        expect(hasClassExports('export const A = 1, B = class {}')).toBe(true)
+      })
+
+      test('mixed exports with class', () => {
+        const code = `
+          export function foo() {}
+          export const bar = 1
+          export class MyClass {}
+        `
+        expect(hasClassExports(code)).toBe(true)
+      })
+    })
+
+    describe('returns false for non-class exports', () => {
+      test('export function foo() {}', () => {
+        expect(hasClassExports('export function foo() {}')).toBe(false)
+      })
+
+      test('export const foo = 1', () => {
+        expect(hasClassExports('export const foo = 1')).toBe(false)
+      })
+
+      test('export default function foo() {}', () => {
+        expect(hasClassExports('export default function foo() {}')).toBe(false)
+      })
+
+      test('export default 42', () => {
+        expect(hasClassExports('export default 42')).toBe(false)
+      })
+
+      test('class Foo {} (no export)', () => {
+        expect(hasClassExports('class Foo {}')).toBe(false)
+      })
+
+      test('const Foo = class {} (no export)', () => {
+        expect(hasClassExports('const Foo = class {}')).toBe(false)
+      })
+
+      test('export { Foo } from "./other" (re-export)', () => {
+        expect(hasClassExports('export { Foo } from "./other"')).toBe(false)
+      })
+
+      test('export * from "./other" (wildcard re-export)', () => {
+        expect(hasClassExports('export * from "./other"')).toBe(false)
+      })
+
+      test('no class keyword at all', () => {
+        expect(
+          hasClassExports('export const x = 1; export function y() {}'),
+        ).toBe(false)
+      })
+
+      test('class in string literal', () => {
+        expect(hasClassExports('export const x = "class Foo {}"')).toBe(false)
+      })
+
+      test('class in comment', () => {
+        expect(hasClassExports('// class Foo {}\nexport const x = 1')).toBe(
+          false,
+        )
+      })
+    })
+  })
+
+  describe('extractImportsFromModule', () => {
+    test('extracts named imports from relative paths', () => {
+      const code = `import { foo, bar } from './utils'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('./utils')).toEqual(new Set(['foo', 'bar']))
+    })
+
+    test('extracts default imports', () => {
+      const code = `import utils from './utils'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('./utils')).toEqual(new Set(['default']))
+    })
+
+    test('extracts mixed default and named imports', () => {
+      const code = `import utils, { foo, bar } from './utils'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('./utils')).toEqual(new Set(['default', 'foo', 'bar']))
+    })
+
+    test('skips type-only imports', () => {
+      const code = `import type { UserType } from './types'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(0)
+    })
+
+    test('skips type-only specifiers in mixed imports', () => {
+      const code = `import { type UserType, processUser } from './utils'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('./utils')).toEqual(new Set(['processUser']))
+    })
+
+    test('skips imports with all type-only specifiers', () => {
+      const code = `import { type UserType, type Config } from './types'`
+      const { imports } = extractImportsFromModule(code)
+      // No entry for this source since all specifiers are type-only
+      expect(imports.has('./types')).toBe(false)
+    })
+
+    test('skips namespace imports', () => {
+      const code = `import * as utils from './utils'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(0)
+    })
+
+    test('skips side-effect imports', () => {
+      const code = `import './polyfill'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(0)
+    })
+
+    test('skips external (node_modules) imports', () => {
+      const code = `import { useState } from 'react'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(0)
+    })
+
+    test('handles multiple import sources', () => {
+      const code = `
+        import { foo } from './utils'
+        import { bar } from './helpers'
+      `
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('./utils')).toEqual(new Set(['foo']))
+      expect(imports.get('./helpers')).toEqual(new Set(['bar']))
+    })
+
+    test('skips imports that already have split-exports query', () => {
+      const code = `import { foo } from './utils?${SPLIT_EXPORTS_QUERY_KEY}=foo'`
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(0)
+    })
+
+    test('extracts alias imports', () => {
+      const code = `
+        import { foo } from './relative'
+        import { bar } from '~/utils/bar'
+        import { baz } from '@/components/Button'
+      `
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(3)
+      expect(imports.get('./relative')).toEqual(new Set(['foo']))
+      expect(imports.get('~/utils/bar')).toEqual(new Set(['bar']))
+      expect(imports.get('@/components/Button')).toEqual(new Set(['baz']))
+    })
+
+    test('skips npm packages', () => {
+      const code = `
+        import { foo } from './relative'
+        import { useState } from 'react'
+        import { useQuery } from '@tanstack/react-query'
+        import { bar } from '~/utils/bar'
+      `
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.size).toBe(2)
+      expect(imports.get('./relative')).toEqual(new Set(['foo']))
+      expect(imports.get('~/utils/bar')).toEqual(new Set(['bar']))
+      // npm packages should be excluded
+      expect(imports.has('react')).toBe(false)
+      expect(imports.has('@tanstack/react-query')).toBe(false)
+    })
+
+    test('extracts hash alias imports', () => {
+      const code = `
+        import { internal } from '#/lib/internal'
+      `
+      const { imports } = extractImportsFromModule(code)
+      expect(imports.get('#/lib/internal')).toEqual(new Set(['internal']))
+    })
+
+    test('handles mixed scenario with aliases, npm packages, and namespace imports', () => {
+      const code = `
+        import { seo } from '~/utils/seo'
+        import type { SEOProps } from '~/utils/seo'
+        import { Link } from '@tanstack/react-router'
+        import { db } from './db'
+        import * as utils from '~/all-utils'
+      `
+      const { imports } = extractImportsFromModule(code)
+      // Should include:
+      // - '~/utils/seo' with 'seo' (type-only specifier excluded)
+      // - './db' with 'db'
+      // Should exclude:
+      // - '@tanstack/react-router' (npm package)
+      // - '~/all-utils' (namespace import)
+      expect(imports.size).toBe(2)
+      expect(imports.get('~/utils/seo')).toEqual(new Set(['seo']))
+      expect(imports.get('./db')).toEqual(new Set(['db']))
+    })
+  })
+
+  describe('transformImports', () => {
+    test('transforms named imports', () => {
+      const code = `import { foo, bar } from './utils'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain(
+        `./utils?${SPLIT_EXPORTS_QUERY_KEY}=bar,foo`,
+      )
+    })
+
+    test('transforms default imports', () => {
+      const code = `import utils from './utils'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain(
+        `./utils?${SPLIT_EXPORTS_QUERY_KEY}=default`,
+      )
+    })
+
+    test('handles mixed imports from same source', () => {
+      const code = `import utils, { foo } from './utils'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain(
+        `./utils?${SPLIT_EXPORTS_QUERY_KEY}=default,foo`,
+      )
+    })
+
+    test('handles mixed type and value imports in same declaration', () => {
+      const code = `import { type UserType, type Config, processUser, formatData } from './utils'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      // Should only include value imports in the query, not type imports
+      expect(result!.code).toContain(
+        `./utils?${SPLIT_EXPORTS_QUERY_KEY}=formatData,processUser`,
+      )
+      // Type imports should not be in the query string
+      expect(result!.code).not.toContain('tss-split-exports=UserType')
+      expect(result!.code).not.toContain('tss-split-exports=Config')
+      expect(result!.code).not.toContain(',UserType')
+      expect(result!.code).not.toContain(',Config')
+    })
+
+    test('preserves existing query params', () => {
+      const code = `import { foo } from './utils?v=123'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain(
+        `./utils?v=123&${SPLIT_EXPORTS_QUERY_KEY}=foo`,
+      )
+    })
+
+    test('does not transform namespace imports', () => {
+      const code = `import * as utils from './utils'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).toBeNull()
+    })
+
+    test('does not transform external imports', () => {
+      const code = `import { useState } from 'react'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).toBeNull()
+    })
+
+    test('does not transform type-only imports', () => {
+      const code = `import type { User } from './types'`
+      const result = transformImports(code, 'test.ts')
+      expect(result).toBeNull()
+    })
+
+    test('does not transform imports with all type-only specifiers', () => {
+      const code = `import { type UserType, type Config } from './types'`
+      const result = transformImports(code, 'test.ts')
+      // No value imports to transform, so return null
+      expect(result).toBeNull()
+    })
+
+    test('transforms multiple import statements', () => {
+      const code = `
+        import { foo } from './utils'
+        import { bar } from './helpers'
+      `
+      const result = transformImports(code, 'test.ts')
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain(`./utils?${SPLIT_EXPORTS_QUERY_KEY}=foo`)
+      expect(result!.code).toContain(`./helpers?${SPLIT_EXPORTS_QUERY_KEY}=bar`)
+    })
+  })
+
+  describe('transformExports', () => {
+    test('returns null when all exports are kept', () => {
+      const code = `
+        export const foo = () => 'foo'
+        export const bar = () => 'bar'
+      `
+      const result = transformExports(code, 'test.ts', new Set(['foo', 'bar']))
+      expect(result).toBeNull()
+    })
+
+    test('keeps only requested named exports', () => {
+      const code = `
+        export const foo = () => 'foo'
+        export const bar = () => 'bar'
+        export const baz = () => 'baz'
+      `
+      const result = transformExports(code, 'test.ts', new Set(['foo']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export const foo')
+      expect(result!.code).not.toContain('export const bar')
+      expect(result!.code).not.toContain('export const baz')
+    })
+
+    test('keeps default export when requested', () => {
+      const code = `
+        export default function main() { return 'main' }
+        export const unused = 'unused'
+      `
+      const result = transformExports(code, 'test.ts', new Set(['default']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export default function main')
+      expect(result!.code).not.toContain('export const unused')
+    })
+
+    test('removes default export when not requested', () => {
+      const code = `
+        export default function main() { return 'main' }
+        export const used = 'used'
+      `
+      const result = transformExports(code, 'test.ts', new Set(['used']))
+      expect(result).not.toBeNull()
+      expect(result!.code).not.toContain('export default')
+      expect(result!.code).toContain('export const used')
+    })
+
+    test('handles re-exports', () => {
+      const code = `
+        export { foo, bar } from './source'
+      `
+      const result = transformExports(code, 'test.ts', new Set(['foo']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('foo')
+      expect(result!.code).not.toContain('bar')
+    })
+
+    test('preserves star re-exports', () => {
+      const code = `
+        export * from './source'
+        export { foo, bar } from './other'
+      `
+      // Keep only 'foo', so 'bar' should be removed but 'export *' should stay
+      const result = transformExports(code, 'test.ts', new Set(['foo']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export * from')
+      expect(result!.code).toContain('foo')
+      expect(result!.code).not.toContain('bar')
+    })
+
+    test('handles multiple declarators in single export', () => {
+      const code = `export const a = 1, b = 2, c = 3`
+      const result = transformExports(code, 'test.ts', new Set(['a', 'c']))
+      expect(result).not.toBeNull()
+      // Both a and c should be exported (may be combined in single declaration)
+      expect(result!.code).toContain('export const a')
+      expect(result!.code).toContain('c = 3')
+      // b should not be exported
+      expect(result!.code).not.toContain('export const b')
+      expect(result!.code).not.toContain('b = 2')
+    })
+
+    test('runs dead code elimination', () => {
+      const code = `
+        const helper = () => 'helper'
+        const unused = () => 'unused'
+        export const foo = () => helper()
+        export const bar = () => unused()
+      `
+      const result = transformExports(code, 'test.ts', new Set(['foo']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('helper')
+      expect(result!.code).not.toContain('unused')
+    })
+
+    test('handles function exports', () => {
+      const code = `
+        export function myFunc() { return 'func' }
+        export function unused() { return 'unused' }
+      `
+      const result = transformExports(code, 'test.ts', new Set(['myFunc']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export function myFunc')
+      expect(result!.code).not.toContain('unused')
+    })
+
+    test('handles class exports', () => {
+      const code = `
+        export class MyClass { getValue() { return 'class' } }
+        export class Unused { getValue() { return 'unused' } }
+      `
+      const result = transformExports(code, 'test.ts', new Set(['MyClass']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export class MyClass')
+      expect(result!.code).not.toContain('export class Unused')
+    })
+
+    test('real-world: eliminates server-only code', () => {
+      const code = `
+        import { db } from './db'
+        
+        export const formatUser = (user) => user.name.toUpperCase()
+        
+        export const getUser = async (id) => db.users.findOne({ id })
+        
+        export const deleteUser = async (id) => db.users.delete({ id })
+      `
+      const result = transformExports(code, 'test.ts', new Set(['formatUser']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export const formatUser')
+      expect(result!.code).not.toContain('getUser')
+      expect(result!.code).not.toContain('deleteUser')
+      // db import should be eliminated by DCE since nothing uses it
+      expect(result!.code).not.toContain('db')
+    })
+  })
+
+  describe('snapshot tests', () => {
+    describe('import transformations', async () => {
+      const testFilesDir = path.resolve(import.meta.dirname, './test-files')
+      const importTestFiles = [
+        'namedImports.ts',
+        'defaultImport.ts',
+        'mixedImports.ts',
+        'mixedTypeValueImports.ts',
+        'typeOnlyImports.ts',
+        'externalImports.ts',
+        'existingQuery.ts',
+      ]
+
+      for (const filename of importTestFiles) {
+        test(`transforms imports in ${filename}`, async () => {
+          const filePath = path.resolve(testFilesDir, filename)
+          const code = await readFile(filePath, 'utf-8')
+          const result = transformImports(code, filename)
+
+          if (result) {
+            await expect(result.code).toMatchFileSnapshot(
+              `./snapshots/imports/${filename}`,
+            )
+          } else {
+            // For files that should not be transformed, create a snapshot of "null"
+            await expect('// No transformation needed').toMatchFileSnapshot(
+              `./snapshots/imports/${filename}`,
+            )
+          }
+        })
+      }
+    })
+
+    describe('export transformations', async () => {
+      const testFilesDir = path.resolve(import.meta.dirname, './test-files')
+
+      const exportTestCases = [
+        { file: 'namedExports.ts', keep: ['foo', 'bar'] },
+        { file: 'defaultExport.ts', keep: ['default'] },
+        { file: 'reExports.ts', keep: ['foo', 'renamedHelper'] },
+        { file: 'starReExport.ts', keep: ['foo'] },
+        { file: 'multipleDeclarators.ts', keep: ['a', 'processA'] },
+        { file: 'functionExports.ts', keep: ['myFunction'] },
+        { file: 'serverOnlyCode.ts', keep: ['formatUser'] },
+      ]
+
+      for (const { file, keep } of exportTestCases) {
+        test(`transforms exports in ${file} keeping [${keep.join(', ')}]`, async () => {
+          const filePath = path.resolve(testFilesDir, file)
+          const code = await readFile(filePath, 'utf-8')
+          const result = transformExports(code, file, new Set(keep))
+
+          expect(result).not.toBeNull()
+          await expect(result!.code).toMatchFileSnapshot(
+            `./snapshots/exports/${file}`,
+          )
+        })
+      }
+    })
+  })
+
+  describe('getAllExportNames', () => {
+    test('extracts named exports from const declarations', () => {
+      const code = `
+        export const foo = 1
+        export const bar = 2
+      `
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['foo', 'bar']))
+      expect(result.hasWildcardReExport).toBe(false)
+    })
+
+    test('extracts multiple declarators from single export', () => {
+      const code = `export const a = 1, b = 2, c = 3`
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['a', 'b', 'c']))
+    })
+
+    test('extracts function exports', () => {
+      const code = `
+        export function myFunc() { return 1 }
+        export function otherFunc() { return 2 }
+      `
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['myFunc', 'otherFunc']))
+    })
+
+    test('extracts class exports', () => {
+      const code = `
+        export class MyClass {}
+        export class OtherClass {}
+      `
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['MyClass', 'OtherClass']))
+    })
+
+    test('extracts default export', () => {
+      const code = `export default function main() { return 1 }`
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['default']))
+    })
+
+    test('extracts re-exports', () => {
+      const code = `export { foo, bar as baz } from './source'`
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['foo', 'baz']))
+    })
+
+    test('extracts export specifiers without source', () => {
+      const code = `
+        const foo = 1
+        const bar = 2
+        export { foo, bar as baz }
+      `
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(new Set(['foo', 'baz']))
+    })
+
+    test('detects wildcard re-exports', () => {
+      const code = `
+        export * from './source'
+        export const foo = 1
+      `
+      const result = getAllExportNames(code)
+      expect(result.hasWildcardReExport).toBe(true)
+      // Still collects the named exports it can enumerate
+      expect(result.exportNames).toContain('foo')
+    })
+
+    test('handles mixed export types', () => {
+      const code = `
+        export const foo = 1
+        export function bar() {}
+        export class Baz {}
+        export default function main() {}
+        export { qux } from './source'
+      `
+      const result = getAllExportNames(code)
+      expect(result.exportNames).toEqual(
+        new Set(['foo', 'bar', 'Baz', 'default', 'qux']),
+      )
+    })
+
+    test('returns the parsed AST for reuse', () => {
+      const code = `export const foo = 1`
+      const result = getAllExportNames(code)
+      expect(result.ast).toBeDefined()
+      expect(result.ast.program).toBeDefined()
+    })
+  })
+
+  describe('transformExports early bailout', () => {
+    test('bails out early when all exports are requested (no transformation)', () => {
+      const code = `
+        export const foo = 1
+        export const bar = 2
+        export function baz() {}
+      `
+      // Request all exports
+      const result = transformExports(
+        code,
+        'test.ts',
+        new Set(['foo', 'bar', 'baz']),
+      )
+      expect(result).toBeNull()
+    })
+
+    test('bails out early when requesting more exports than exist', () => {
+      const code = `
+        export const foo = 1
+        export const bar = 2
+      `
+      // Request all exports plus extra ones that don't exist
+      const result = transformExports(
+        code,
+        'test.ts',
+        new Set(['foo', 'bar', 'extra', 'nonexistent']),
+      )
+      expect(result).toBeNull()
+    })
+
+    test('does not bail out when some exports are not requested', () => {
+      const code = `
+        export const foo = 1
+        export const bar = 2
+        export const baz = 3
+      `
+      // Only request some exports
+      const result = transformExports(code, 'test.ts', new Set(['foo', 'bar']))
+      expect(result).not.toBeNull()
+      expect(result!.code).toContain('export const foo')
+      expect(result!.code).toContain('export const bar')
+      expect(result!.code).not.toContain('export const baz')
+    })
+
+    test('does not bail out with wildcard re-exports even if named exports match', () => {
+      const code = `
+        export * from './source'
+        export const foo = 1
+      `
+      // We can't know what's in 'export *', so we must transform
+      // Even though 'foo' is in the request, we can't bail out
+      const result = transformExports(code, 'test.ts', new Set(['foo']))
+      // Should still transform because we can't enumerate all exports
+      // The result could be null if foo is kept and export * is preserved as-is
+      // Let's just verify it handles this case without errors
+      // (The implementation preserves export * and removes non-requested named exports)
+      if (result) {
+        expect(result.code).toContain('export * from')
+      }
+    })
+
+    test('bails out for default export only module when default is requested', () => {
+      const code = `export default function main() { return 1 }`
+      const result = transformExports(code, 'test.ts', new Set(['default']))
+      expect(result).toBeNull()
+    })
+
+    test('bails out for complex module when all exports are requested', () => {
+      const code = `
+        const helper = () => 'helper'
+        export const foo = () => helper()
+        export function bar() { return 'bar' }
+        export class Baz {}
+        export default { foo: 'default' }
+        export { qux as quux } from './other'
+      `
+      const result = transformExports(
+        code,
+        'test.ts',
+        new Set(['foo', 'bar', 'Baz', 'default', 'quux']),
+      )
+      expect(result).toBeNull()
+    })
+  })
+})
+
+describe('split-exports-plugin utils', () => {
+  describe('isParseableFile', () => {
+    const parseableExtensions = [
+      '.js',
+      '.jsx',
+      '.ts',
+      '.tsx',
+      '.mjs',
+      '.mts',
+      '.cjs',
+      '.cts',
+    ]
+    const nonParseableExtensions = [
+      '.css',
+      '.scss',
+      '.sass',
+      '.less',
+      '.png',
+      '.jpg',
+      '.svg',
+      '.woff2',
+      '.json',
+    ]
+
+    test.each(parseableExtensions)('returns true for %s files', (ext) => {
+      expect(isParseableFile(`/path/to/file${ext}`)).toBe(true)
+    })
+
+    test.each(nonParseableExtensions)('returns false for %s files', (ext) => {
+      expect(isParseableFile(`/path/to/file${ext}`)).toBe(false)
+    })
+
+    describe('handles query strings', () => {
+      test('strips query string before checking extension', () => {
+        expect(isParseableFile('/path/to/file.ts?v=123')).toBe(true)
+      })
+
+      test('correctly identifies CSS with query string', () => {
+        expect(isParseableFile('/path/to/styles.css?direct')).toBe(false)
+      })
+
+      test('correctly identifies CSS with multiple query params', () => {
+        expect(
+          isParseableFile(
+            '/path/to/styles.css?direct&tss-split-exports=default',
+          ),
+        ).toBe(false)
+      })
+    })
+
+    describe('edge cases', () => {
+      test('returns true for files without extension (virtual modules)', () => {
+        expect(isParseableFile('/virtual/module')).toBe(true)
+      })
+
+      test('handles uppercase extensions', () => {
+        expect(isParseableFile('/path/to/file.TS')).toBe(true)
+      })
+
+      test('handles mixed case extensions', () => {
+        expect(isParseableFile('/path/to/file.Tsx')).toBe(true)
+      })
+    })
+  })
+
+  describe('shouldExclude', () => {
+    describe('node_modules exclusion', () => {
+      test('excludes files in node_modules', () => {
+        expect(
+          shouldExclude(
+            '/project/node_modules/lodash/index.js',
+            undefined,
+            undefined,
+          ),
+        ).toBe(true)
+      })
+
+      test('excludes files in nested node_modules', () => {
+        expect(
+          shouldExclude(
+            '/project/packages/foo/node_modules/bar/index.js',
+            undefined,
+            undefined,
+          ),
+        ).toBe(true)
+      })
+    })
+
+    describe('srcDirectory filtering', () => {
+      test('includes files inside srcDirectory', () => {
+        expect(
+          shouldExclude(
+            '/project/src/components/Button.tsx',
+            '/project/src',
+            undefined,
+          ),
+        ).toBe(false)
+      })
+
+      test('includes files in nested directories inside srcDirectory', () => {
+        expect(
+          shouldExclude(
+            '/project/src/features/auth/Login.tsx',
+            '/project/src',
+            undefined,
+          ),
+        ).toBe(false)
+      })
+
+      test('excludes files outside srcDirectory', () => {
+        expect(
+          shouldExclude(
+            '/monorepo/packages/react-start/src/index.ts',
+            '/project/src',
+            undefined,
+          ),
+        ).toBe(true)
+      })
+
+      test('excludes files in sibling directories', () => {
+        expect(
+          shouldExclude('/project/lib/utils.ts', '/project/src', undefined),
+        ).toBe(true)
+      })
+
+      test('handles query strings correctly', () => {
+        expect(
+          shouldExclude(
+            '/project/src/utils.ts?v=123',
+            '/project/src',
+            undefined,
+          ),
+        ).toBe(false)
+      })
+
+      test('handles query strings for excluded files', () => {
+        expect(
+          shouldExclude('/other/path/file.ts?v=123', '/project/src', undefined),
+        ).toBe(true)
+      })
+
+      test('works without srcDirectory (fallback behavior)', () => {
+        // When srcDirectory is undefined, should not exclude (except node_modules)
+        expect(
+          shouldExclude('/some/random/path/file.ts', undefined, undefined),
+        ).toBe(false)
+      })
+
+      test('still excludes node_modules even when srcDirectory is set', () => {
+        expect(
+          shouldExclude(
+            '/project/src/node_modules/bad-package/index.js',
+            '/project/src',
+            undefined,
+          ),
+        ).toBe(true)
+      })
+    })
+
+    describe('custom exclude patterns', () => {
+      test('excludes files matching string pattern', () => {
+        expect(
+          shouldExclude('/project/src/generated/types.ts', '/project/src', [
+            'generated',
+          ]),
+        ).toBe(true)
+      })
+
+      test('excludes files matching regex pattern', () => {
+        expect(
+          shouldExclude(
+            '/project/src/components/Button.test.tsx',
+            '/project/src',
+            [/\.test\./],
+          ),
+        ).toBe(true)
+      })
+
+      test('does not exclude files not matching patterns', () => {
+        expect(
+          shouldExclude('/project/src/components/Button.tsx', '/project/src', [
+            'generated',
+            /\.test\./,
+          ]),
+        ).toBe(false)
+      })
+    })
+
+    describe('path normalization', () => {
+      test('normalizes paths with trailing slashes', () => {
+        // srcDirectory might have trailing slash, file path won't
+        expect(
+          shouldExclude('/project/src/utils.ts', '/project/src/', undefined),
+        ).toBe(false)
+      })
+
+      test('handles paths with double slashes', () => {
+        expect(
+          shouldExclude('/project//src/utils.ts', '/project/src', undefined),
+        ).toBe(false)
+      })
+    })
+  })
+})
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultExport.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultExport.ts
new file mode 100644
index 0000000000..cb497bf5ab
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultExport.ts
@@ -0,0 +1,8 @@
+// Test file: Default export
+const helper = () => 'helper'
+
+export default function main() {
+  return helper()
+}
+
+export const unused = () => 'unused'
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultImport.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultImport.ts
new file mode 100644
index 0000000000..b4cd8edfe6
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/defaultImport.ts
@@ -0,0 +1,7 @@
+// Test file: Default import from relative module
+import utils from './utils'
+import config from '../config'
+
+export function main() {
+  return utils.process(config)
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/existingQuery.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/existingQuery.ts
new file mode 100644
index 0000000000..dbc77319cb
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/existingQuery.ts
@@ -0,0 +1,7 @@
+// Test file: Imports with existing query string
+import { foo } from './utils?v=123'
+import { bar } from './helpers'
+
+export function main() {
+  return foo() + bar()
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/externalImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/externalImports.ts
new file mode 100644
index 0000000000..30cc2bba88
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/externalImports.ts
@@ -0,0 +1,8 @@
+// Test file: External (node_modules) imports should be skipped
+import { useState } from 'react'
+import { foo } from './utils'
+
+export function Component() {
+  const [state] = useState(foo())
+  return state
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/functionExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/functionExports.ts
new file mode 100644
index 0000000000..b0ded78c9b
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/functionExports.ts
@@ -0,0 +1,10 @@
+// Test file: Multiple function exports
+export function myFunction() {
+  return 'function'
+}
+
+export function anotherFunction() {
+  return 'another'
+}
+
+export const unused = 'unused'
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedImports.ts
new file mode 100644
index 0000000000..127025f7d0
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedImports.ts
@@ -0,0 +1,6 @@
+// Test file: Mixed default and named imports
+import utils, { foo, bar } from './utils'
+
+export function main() {
+  return utils.process() + foo() + bar()
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedTypeValueImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedTypeValueImports.ts
new file mode 100644
index 0000000000..19d34ddca7
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/mixedTypeValueImports.ts
@@ -0,0 +1,7 @@
+// Test file: Mixed type and value imports in same declaration
+import { type UserType, type Config, processUser, formatData } from './utils'
+import { type HelperType, helper } from './helpers'
+
+export function main(user: UserType, config: Config) {
+  return processUser(user) + formatData(config) + helper()
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/multipleDeclarators.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/multipleDeclarators.ts
new file mode 100644
index 0000000000..f05376c166
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/multipleDeclarators.ts
@@ -0,0 +1,8 @@
+// Test file: Export with declaration containing multiple declarators
+export const a = 1,
+  b = 2,
+  c = 3
+
+export function processA() {
+  return a * 2
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedExports.ts
new file mode 100644
index 0000000000..40da353840
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedExports.ts
@@ -0,0 +1,7 @@
+// Test file: Multiple named exports - some should be removed
+export const foo = () => 'foo'
+export const bar = () => 'bar'
+export const baz = () => 'baz'
+
+const internal = 'internal'
+export const qux = () => internal
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedImports.ts
new file mode 100644
index 0000000000..c8e8989e38
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namedImports.ts
@@ -0,0 +1,7 @@
+// Test file: Named imports from relative module
+import { foo, bar } from './utils'
+import { helper } from '../shared/helpers'
+
+export function main() {
+  return foo() + bar() + helper()
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/namespaceImport.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namespaceImport.ts
new file mode 100644
index 0000000000..c4275444a9
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/namespaceImport.ts
@@ -0,0 +1,6 @@
+// Test file: Namespace imports should be skipped
+import * as utils from './utils'
+
+export function main() {
+  return utils.foo() + utils.bar()
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/reExports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/reExports.ts
new file mode 100644
index 0000000000..2957057e40
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/reExports.ts
@@ -0,0 +1,4 @@
+// Test file: Re-exports from other modules
+export { foo, bar } from './source'
+export { helper as renamedHelper } from './helpers'
+export { default as utils } from './utils'
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/serverOnlyCode.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/serverOnlyCode.ts
new file mode 100644
index 0000000000..4a5f5e88f8
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/serverOnlyCode.ts
@@ -0,0 +1,17 @@
+// Test file: Complex module with server-only code (real-world scenario)
+import { db } from './db' // server-only
+
+// This is isomorphic - used on both client and server
+export const formatUser = (user: { name: string }) => {
+  return user.name.toUpperCase()
+}
+
+// This is server-only - should be eliminated when only formatUser is imported
+export const getUser = async (id: string) => {
+  return db.users.findOne({ id })
+}
+
+// This is also server-only
+export const deleteUser = async (id: string) => {
+  return db.users.delete({ id })
+}
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/starReExport.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/starReExport.ts
new file mode 100644
index 0000000000..01d0f7b8cb
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/starReExport.ts
@@ -0,0 +1,3 @@
+// Test file: Star re-exports should be left as-is
+export * from './source'
+export { foo, bar } from './other'
diff --git a/packages/start-plugin-core/tests/split-exports-plugin/test-files/typeOnlyImports.ts b/packages/start-plugin-core/tests/split-exports-plugin/test-files/typeOnlyImports.ts
new file mode 100644
index 0000000000..ff422a0bb1
--- /dev/null
+++ b/packages/start-plugin-core/tests/split-exports-plugin/test-files/typeOnlyImports.ts
@@ -0,0 +1,7 @@
+// Test file: Type-only imports should be skipped
+import type { UserType } from './types'
+import { processUser } from './utils'
+
+export function main(user: UserType) {
+  return processUser(user)
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f20ebf80c..cf6995fb5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2039,6 +2039,67 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.8.3)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/react-start/split-exports:
+    dependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/react-router-devtools':
+        specifier: workspace:^
+        version: link:../../../packages/react-router-devtools
+      '@tanstack/react-start':
+        specifier: workspace:*
+        version: link:../../../packages/react-start
+      react:
+        specifier: ^19.2.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.2.0
+        version: 19.2.0(react@19.2.0)
+      tailwind-merge:
+        specifier: ^2.6.0
+        version: 2.6.0
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.56.1
+        version: 1.56.1
+      '@tailwindcss/postcss':
+        specifier: ^4.1.15
+        version: 4.1.15
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 22.10.2
+        version: 22.10.2
+      '@types/react':
+        specifier: ^19.2.2
+        version: 19.2.2
+      '@types/react-dom':
+        specifier: ^19.2.2
+        version: 19.2.2(@types/react@19.2.2)
+      '@vitejs/plugin-react':
+        specifier: ^4.3.4
+        version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      postcss:
+        specifier: ^8.5.1
+        version: 8.5.6
+      srvx:
+        specifier: ^0.8.6
+        version: 0.8.15
+      tailwindcss:
+        specifier: ^4.1.17
+        version: 4.1.17
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.2
+      vite:
+        specifier: ^7.1.7
+        version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/react-start/virtual-routes:
     dependencies:
       '@tanstack/react-router':
