diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts
index 0f14d59ad7b..ad1492ba6d6 100644
--- a/e2e/react-start/server-functions/src/routeTree.gen.ts
+++ b/e2e/react-start/server-functions/src/routeTree.gen.ts
@@ -34,6 +34,7 @@ import { Route as CookiesIndexRouteImport } from './routes/cookies/index'
import { Route as AbortSignalIndexRouteImport } from './routes/abort-signal/index'
import { Route as RedirectTestTargetRouteImport } from './routes/redirect-test/target'
import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-test-ssr/target'
+import { Route as MiddlewareServerImportMiddlewareRouteImport } from './routes/middleware/server-import-middleware'
import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware'
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
@@ -166,6 +167,12 @@ const RedirectTestSsrTargetRoute = RedirectTestSsrTargetRouteImport.update({
path: '/redirect-test-ssr/target',
getParentRoute: () => rootRouteImport,
} as any)
+const MiddlewareServerImportMiddlewareRoute =
+ MiddlewareServerImportMiddlewareRouteImport.update({
+ id: '/middleware/server-import-middleware',
+ path: '/middleware/server-import-middleware',
+ getParentRoute: () => rootRouteImport,
+ } as any)
const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({
id: '/middleware/send-serverFn',
path: '/middleware/send-serverFn',
@@ -221,6 +228,7 @@ export interface FileRoutesByFullPath {
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
'/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
'/redirect-test/target': typeof RedirectTestTargetRoute
'/abort-signal': typeof AbortSignalIndexRoute
@@ -254,6 +262,7 @@ export interface FileRoutesByTo {
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
'/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
'/redirect-test/target': typeof RedirectTestTargetRoute
'/abort-signal': typeof AbortSignalIndexRoute
@@ -288,6 +297,7 @@ export interface FileRoutesById {
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
'/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
'/redirect-test/target': typeof RedirectTestTargetRoute
'/abort-signal/': typeof AbortSignalIndexRoute
@@ -323,6 +333,7 @@ export interface FileRouteTypes {
| '/middleware/client-middleware-router'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
+ | '/middleware/server-import-middleware'
| '/redirect-test-ssr/target'
| '/redirect-test/target'
| '/abort-signal'
@@ -356,6 +367,7 @@ export interface FileRouteTypes {
| '/middleware/client-middleware-router'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
+ | '/middleware/server-import-middleware'
| '/redirect-test-ssr/target'
| '/redirect-test/target'
| '/abort-signal'
@@ -389,6 +401,7 @@ export interface FileRouteTypes {
| '/middleware/client-middleware-router'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
+ | '/middleware/server-import-middleware'
| '/redirect-test-ssr/target'
| '/redirect-test/target'
| '/abort-signal/'
@@ -423,6 +436,7 @@ export interface RootRouteChildren {
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
+ MiddlewareServerImportMiddlewareRoute: typeof MiddlewareServerImportMiddlewareRoute
RedirectTestSsrTargetRoute: typeof RedirectTestSsrTargetRoute
RedirectTestTargetRoute: typeof RedirectTestTargetRoute
AbortSignalIndexRoute: typeof AbortSignalIndexRoute
@@ -613,6 +627,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RedirectTestSsrTargetRouteImport
parentRoute: typeof rootRouteImport
}
+ '/middleware/server-import-middleware': {
+ id: '/middleware/server-import-middleware'
+ path: '/middleware/server-import-middleware'
+ fullPath: '/middleware/server-import-middleware'
+ preLoaderRoute: typeof MiddlewareServerImportMiddlewareRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/middleware/send-serverFn': {
id: '/middleware/send-serverFn'
path: '/middleware/send-serverFn'
@@ -679,6 +700,7 @@ const rootRouteChildren: RootRouteChildren = {
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
+ MiddlewareServerImportMiddlewareRoute: MiddlewareServerImportMiddlewareRoute,
RedirectTestSsrTargetRoute: RedirectTestSsrTargetRoute,
RedirectTestTargetRoute: RedirectTestTargetRoute,
AbortSignalIndexRoute: AbortSignalIndexRoute,
diff --git a/e2e/react-start/server-functions/src/routes/middleware/index.tsx b/e2e/react-start/server-functions/src/routes/middleware/index.tsx
index 1119e6bfea9..060d16f6359 100644
--- a/e2e/react-start/server-functions/src/routes/middleware/index.tsx
+++ b/e2e/react-start/server-functions/src/routes/middleware/index.tsx
@@ -33,6 +33,14 @@ function RouteComponent() {
Request Middleware in combination with server function
+
+
+ Server imports in middleware are stripped from client build
+
+
)
diff --git a/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx b/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx
new file mode 100644
index 00000000000..cdcb8966c6e
--- /dev/null
+++ b/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx
@@ -0,0 +1,82 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createMiddleware, createServerFn } from '@tanstack/react-start'
+import { getRequestHeaders } from '@tanstack/react-start/server'
+import React from 'react'
+
+/**
+ * This test verifies that server-only imports (like getRequestHeaders from @tanstack/react-start/server)
+ * are properly removed from the client bundle when used inside createMiddleware().server().
+ *
+ * If the .server() part is not stripped from the client build, this will fail with:
+ * "Module node:async_hooks has been externalized for browser compatibility"
+ * because @tanstack/react-start/server uses node:async_hooks internally.
+ */
+const serverImportMiddleware = createMiddleware({ type: 'function' }).server(
+ async ({ next }) => {
+ // Use a server-only import - this should be stripped from client build
+ const headers = getRequestHeaders()
+ const testHeader = headers.get('x-test-middleware') ?? 'missing'
+
+ console.log('[server-import-middleware] X-Test-Middleware:', testHeader)
+
+ return next({
+ context: {
+ testHeader,
+ },
+ })
+ },
+)
+
+const serverFn = createServerFn()
+ .middleware([serverImportMiddleware])
+ .handler(async ({ context }) => {
+ return { testHeader: context.testHeader }
+ })
+
+export const Route = createFileRoute('/middleware/server-import-middleware')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const [result, setResult] = React.useState<{ testHeader: string } | null>(
+ null,
+ )
+ const [error, setError] = React.useState(null)
+
+ async function handleClick() {
+ try {
+ const data = await serverFn({
+ headers: { 'x-test-middleware': 'test-header-value' },
+ })
+ setResult(data)
+ setError(null)
+ } catch (e) {
+ setResult(null)
+ setError(e instanceof Error ? e.message : String(e))
+ }
+ }
+
+ return (
+
+
Server Import in Middleware Test
+
+ This test verifies that server-only imports (getRequestHeaders) inside
+ createMiddleware().server() are properly stripped from the client build.
+
+
+ {result && (
+
+ {result.testHeader}
+
+ )}
+ {error && (
+
Error: {error}
+ )}
+
+ )
+}
diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts
index 614e6d6b59e..5848f73a612 100644
--- a/e2e/react-start/server-functions/tests/server-functions.spec.ts
+++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts
@@ -615,3 +615,23 @@ test('nested star re-exported server function factory middleware executes correc
page.getByTestId('fn-comparison-nestedReexportedFactoryFn'),
).toContainText('equal')
})
+
+test('server-only imports in middleware.server() are stripped from client build', async ({
+ page,
+}) => {
+ // This test verifies that server-only imports (like getRequestHeaders from @tanstack/react-start/server)
+ // inside createMiddleware().server() are properly stripped from the client build.
+ // If the .server() part is not removed, the build would fail with node:async_hooks externalization errors.
+ // The fact that this page loads at all proves the server code was stripped correctly.
+ await page.goto('/middleware/server-import-middleware')
+
+ await page.waitForLoadState('networkidle')
+
+ // Click the button to call the server function with middleware
+ await page.getByTestId('test-server-import-middleware-btn').click()
+
+ // Wait for the result - should contain our custom test header value
+ await expect(
+ page.getByTestId('server-import-middleware-result'),
+ ).toContainText('test-header-value')
+})
diff --git a/labeler-config.yml b/labeler-config.yml
index d09a5367098..bd2b746e121 100644
--- a/labeler-config.yml
+++ b/labeler-config.yml
@@ -82,6 +82,9 @@
'package: start-client-core':
- changed-files:
- any-glob-to-any-file: 'packages/start-client-core/**/*'
+'package: start-fn-stubs':
+ - changed-files:
+ - any-glob-to-any-file: 'packages/start-fn-stubs/**/*'
'package: start-plugin-core':
- changed-files:
- any-glob-to-any-file: 'packages/start-plugin-core/**/*'
diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json
index 02d233566a2..d860d14da7c 100644
--- a/packages/start-client-core/package.json
+++ b/packages/start-client-core/package.json
@@ -80,6 +80,7 @@
},
"dependencies": {
"@tanstack/router-core": "workspace:*",
+ "@tanstack/start-fn-stubs": "workspace:*",
"@tanstack/start-storage-context": "workspace:*",
"seroval": "^1.4.1",
"tiny-invariant": "^1.3.3",
diff --git a/packages/start-client-core/src/getGlobalStartContext.ts b/packages/start-client-core/src/getGlobalStartContext.ts
index 20c1bcd6da3..3e1af51361a 100644
--- a/packages/start-client-core/src/getGlobalStartContext.ts
+++ b/packages/start-client-core/src/getGlobalStartContext.ts
@@ -1,5 +1,5 @@
import { getStartContext } from '@tanstack/start-storage-context'
-import { createIsomorphicFn } from './createIsomorphicFn'
+import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
import type { AssignAllServerRequestContext } from './createMiddleware'
import type { Expand, Register } from '@tanstack/router-core'
diff --git a/packages/start-client-core/src/getRouterInstance.ts b/packages/start-client-core/src/getRouterInstance.ts
index 501970ed0b0..d1fc9d6301c 100644
--- a/packages/start-client-core/src/getRouterInstance.ts
+++ b/packages/start-client-core/src/getRouterInstance.ts
@@ -1,5 +1,5 @@
import { getStartContext } from '@tanstack/start-storage-context'
-import { createIsomorphicFn } from './createIsomorphicFn'
+import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
import type { Awaitable, RegisteredRouter } from '@tanstack/router-core'
export const getRouterInstance: () => Awaitable =
diff --git a/packages/start-client-core/src/getStartContextServerOnly.ts b/packages/start-client-core/src/getStartContextServerOnly.ts
index 78787a49825..4b1ec43c04d 100644
--- a/packages/start-client-core/src/getStartContextServerOnly.ts
+++ b/packages/start-client-core/src/getStartContextServerOnly.ts
@@ -1,4 +1,4 @@
import { getStartContext } from '@tanstack/start-storage-context'
-import { createServerOnlyFn } from './envOnly'
+import { createServerOnlyFn } from '@tanstack/start-fn-stubs'
export const getStartContextServerOnly = createServerOnlyFn(getStartContext)
diff --git a/packages/start-client-core/src/getStartOptions.ts b/packages/start-client-core/src/getStartOptions.ts
index 7c08e1f49a6..d290934b5fb 100644
--- a/packages/start-client-core/src/getStartOptions.ts
+++ b/packages/start-client-core/src/getStartOptions.ts
@@ -1,5 +1,5 @@
import { getStartContext } from '@tanstack/start-storage-context'
-import { createIsomorphicFn } from './createIsomorphicFn'
+import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
import type { AnyStartInstanceOptions } from './createStart'
export const getStartOptions: () => AnyStartInstanceOptions | undefined =
diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
index 015ef25987d..eb5f7eb8f0e 100644
--- a/packages/start-client-core/src/index.tsx
+++ b/packages/start-client-core/src/index.tsx
@@ -4,12 +4,13 @@ export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client'
export {
createIsomorphicFn,
+ createServerOnlyFn,
+ createClientOnlyFn,
type IsomorphicFn,
type ServerOnlyFn,
type ClientOnlyFn,
type IsomorphicFnBase,
-} from './createIsomorphicFn'
-export { createServerOnlyFn, createClientOnlyFn } from './envOnly'
+} from '@tanstack/start-fn-stubs'
export { createServerFn } from './createServerFn'
export {
createMiddleware,
diff --git a/packages/start-fn-stubs/eslint.config.js b/packages/start-fn-stubs/eslint.config.js
new file mode 100644
index 00000000000..f4c18ebca2f
--- /dev/null
+++ b/packages/start-fn-stubs/eslint.config.js
@@ -0,0 +1,14 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ {
+ files: ['**/*.{ts,tsx}'],
+ },
+ {
+ plugins: {},
+ rules: {},
+ },
+]
diff --git a/packages/start-fn-stubs/package.json b/packages/start-fn-stubs/package.json
new file mode 100644
index 00000000000..432c0ee95bf
--- /dev/null
+++ b/packages/start-fn-stubs/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@tanstack/start-fn-stubs",
+ "version": "1.142.8",
+ "description": "Stub functions for TanStack Start isomorphic and environment-specific functions",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TanStack/router.git",
+ "directory": "packages/start-fn-stubs"
+ },
+ "homepage": "https://tanstack.com/start",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "tanstack",
+ "start",
+ "isomorphic",
+ "server",
+ "client"
+ ],
+ "scripts": {
+ "clean": "rimraf ./dist && rimraf ./coverage",
+ "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
+ "test:unit": "vitest",
+ "test:unit:dev": "vitest --watch",
+ "test:eslint": "eslint ./src",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+ "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
+ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
+ "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
+ "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
+ "test:types:ts59": "tsc",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=22.12.0"
+ }
+}
diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-fn-stubs/src/createIsomorphicFn.ts
similarity index 100%
rename from packages/start-client-core/src/createIsomorphicFn.ts
rename to packages/start-fn-stubs/src/createIsomorphicFn.ts
diff --git a/packages/start-client-core/src/envOnly.ts b/packages/start-fn-stubs/src/envOnly.ts
similarity index 100%
rename from packages/start-client-core/src/envOnly.ts
rename to packages/start-fn-stubs/src/envOnly.ts
diff --git a/packages/start-fn-stubs/src/index.ts b/packages/start-fn-stubs/src/index.ts
new file mode 100644
index 00000000000..5ca646d9d1c
--- /dev/null
+++ b/packages/start-fn-stubs/src/index.ts
@@ -0,0 +1,9 @@
+export {
+ createIsomorphicFn,
+ type IsomorphicFn,
+ type ServerOnlyFn,
+ type ClientOnlyFn,
+ type IsomorphicFnBase,
+} from './createIsomorphicFn'
+
+export { createServerOnlyFn, createClientOnlyFn } from './envOnly'
diff --git a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts b/packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts
similarity index 97%
rename from packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts
rename to packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts
index 89f427d8c64..7c6c3b040a0 100644
--- a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts
+++ b/packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts
@@ -1,5 +1,5 @@
import { expectTypeOf, test } from 'vitest'
-import { createIsomorphicFn } from '../createIsomorphicFn'
+import { createIsomorphicFn } from '../src/createIsomorphicFn'
test('createIsomorphicFn with no implementations', () => {
const fn = createIsomorphicFn()
diff --git a/packages/start-client-core/src/tests/envOnly.test-d.ts b/packages/start-fn-stubs/tests/envOnly.test-d.ts
similarity index 94%
rename from packages/start-client-core/src/tests/envOnly.test-d.ts
rename to packages/start-fn-stubs/tests/envOnly.test-d.ts
index 4a47ddf946b..dd83e4f5617 100644
--- a/packages/start-client-core/src/tests/envOnly.test-d.ts
+++ b/packages/start-fn-stubs/tests/envOnly.test-d.ts
@@ -1,5 +1,5 @@
import { expectTypeOf, test } from 'vitest'
-import { createClientOnlyFn, createServerOnlyFn } from '../envOnly'
+import { createClientOnlyFn, createServerOnlyFn } from '../src/envOnly'
const inputFn = () => 'output'
diff --git a/packages/start-fn-stubs/tsconfig.json b/packages/start-fn-stubs/tsconfig.json
new file mode 100644
index 00000000000..0484835e6b5
--- /dev/null
+++ b/packages/start-fn-stubs/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "esnext"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/packages/start-fn-stubs/vite.config.ts b/packages/start-fn-stubs/vite.config.ts
new file mode 100644
index 00000000000..3c6a120dafc
--- /dev/null
+++ b/packages/start-fn-stubs/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import packageJson from './package.json'
+
+const config = defineConfig({
+ test: {
+ typecheck: { enabled: true },
+ name: packageJson.name,
+ watch: false,
+ },
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ srcDir: './src',
+ entry: './src/index.ts',
+ cjs: false,
+ }),
+)
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 f201d17a794..9ad4c4e5aa8 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
@@ -8,6 +8,8 @@ import {
} from 'babel-dead-code-elimination'
import { handleCreateServerFn } from './handleCreateServerFn'
import { handleCreateMiddleware } from './handleCreateMiddleware'
+import { handleCreateIsomorphicFn } from './handleCreateIsomorphicFn'
+import { handleEnvOnlyFn } from './handleEnvOnly'
import type { MethodChainPaths, RewriteCandidate } from './types'
type Binding =
@@ -30,36 +32,68 @@ type ExportEntry =
type Kind = 'None' | `Root` | `Builder` | LookupKind
-export type LookupKind = 'ServerFn' | 'Middleware'
+export type LookupKind =
+ | 'ServerFn'
+ | 'Middleware'
+ | 'IsomorphicFn'
+ | 'ServerOnlyFn'
+ | 'ClientOnlyFn'
+
+// Detection strategy for each kind
+type MethodChainSetup = {
+ type: 'methodChain'
+ candidateCallIdentifier: Set
+ // If true, a call to the root function (e.g., createIsomorphicFn()) is also a candidate
+ // even without chained method calls. This is used for IsomorphicFn which can be
+ // called without .client() or .server() (resulting in a no-op function).
+ allowRootAsCandidate?: boolean
+}
+type DirectCallSetup = { type: 'directCall' }
-const LookupSetup: Record<
- LookupKind,
- { candidateCallIdentifier: Set }
-> = {
- ServerFn: { candidateCallIdentifier: new Set(['handler']) },
+const LookupSetup: Record = {
+ ServerFn: {
+ type: 'methodChain',
+ candidateCallIdentifier: new Set(['handler']),
+ },
Middleware: {
+ type: 'methodChain',
candidateCallIdentifier: new Set(['server', 'client', 'createMiddlewares']),
},
+ IsomorphicFn: {
+ type: 'methodChain',
+ candidateCallIdentifier: new Set(['server', 'client']),
+ allowRootAsCandidate: true, // createIsomorphicFn() alone is valid (returns no-op)
+ },
+ ServerOnlyFn: { type: 'directCall' },
+ ClientOnlyFn: { type: 'directCall' },
}
-// Pre-computed map: identifier name -> LookupKind for fast candidate detection
-const IdentifierToKind = new Map()
+// Pre-computed map: identifier name -> Set for fast candidate detection (method chain only)
+// Multiple kinds can share the same identifier (e.g., 'server' and 'client' are used by both Middleware and IsomorphicFn)
+const IdentifierToKinds = new Map>()
for (const [kind, setup] of Object.entries(LookupSetup) as Array<
- [LookupKind, { candidateCallIdentifier: Set }]
+ [LookupKind, MethodChainSetup | DirectCallSetup]
>) {
- for (const id of setup.candidateCallIdentifier) {
- IdentifierToKind.set(id, kind)
+ if (setup.type === 'methodChain') {
+ for (const id of setup.candidateCallIdentifier) {
+ let kinds = IdentifierToKinds.get(id)
+ if (!kinds) {
+ kinds = new Set()
+ IdentifierToKinds.set(id, kinds)
+ }
+ kinds.add(kind)
+ }
}
}
export type LookupConfig = {
libName: string
rootExport: string
+ kind: LookupKind | 'Root' // 'Root' for builder pattern, LookupKind for direct call
}
+
interface ModuleInfo {
id: string
- code: string
- ast: ReturnType
bindings: Map
exports: Map
// Track `export * from './module'` declarations for re-export resolution
@@ -70,6 +104,9 @@ export class ServerFnCompiler {
private moduleCache = new Map()
private initialized = false
private validLookupKinds: Set
+ // Precomputed flags for candidate detection (avoid recomputing on each collectCandidates call)
+ private hasDirectCallKinds: boolean
+ private hasRootAsCandidateKinds: boolean
// Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
// Maps: libName → (exportName → Kind)
// This allows O(1) resolution for the common case without async resolveId calls
@@ -85,12 +122,44 @@ export class ServerFnCompiler {
},
) {
this.validLookupKinds = options.lookupKinds
+
+ // Precompute flags for candidate detection
+ this.hasDirectCallKinds = false
+ this.hasRootAsCandidateKinds = false
+ for (const kind of options.lookupKinds) {
+ const setup = LookupSetup[kind]
+ if (setup.type === 'directCall') {
+ this.hasDirectCallKinds = true
+ } else if (setup.allowRootAsCandidate) {
+ this.hasRootAsCandidateKinds = true
+ }
+ }
}
- private async init(id: string) {
+ private async init() {
+ // Register internal stub package exports for recognition.
+ // These don't need module resolution - only the knownRootImports fast path.
+ this.knownRootImports.set(
+ '@tanstack/start-fn-stubs',
+ new Map([
+ ['createIsomorphicFn', 'IsomorphicFn'],
+ ['createServerOnlyFn', 'ServerOnlyFn'],
+ ['createClientOnlyFn', 'ClientOnlyFn'],
+ ]),
+ )
+
await Promise.all(
this.options.lookupConfigurations.map(async (config) => {
- const libId = await this.options.resolveId(config.libName, id)
+ // Populate the fast lookup map for direct imports (by package name)
+ // This allows O(1) recognition of imports from known packages.
+ let libExports = this.knownRootImports.get(config.libName)
+ if (!libExports) {
+ libExports = new Map()
+ this.knownRootImports.set(config.libName, libExports)
+ }
+ libExports.set(config.rootExport, config.kind)
+
+ const libId = await this.options.resolveId(config.libName)
if (!libId) {
throw new Error(`could not resolve "${config.libName}"`)
}
@@ -98,10 +167,8 @@ export class ServerFnCompiler {
if (!rootModule) {
// insert root binding
rootModule = {
- ast: null as any,
bindings: new Map(),
exports: new Map(),
- code: '',
id: libId,
reExportAllSources: [],
}
@@ -119,18 +186,10 @@ export class ServerFnCompiler {
})
rootModule.bindings.set(config.rootExport, {
type: 'var',
- init: t.identifier(config.rootExport),
- resolvedKind: `Root` satisfies Kind,
+ init: null, // Not needed since resolvedKind is set
+ resolvedKind: config.kind satisfies Kind,
})
this.moduleCache.set(libId, rootModule)
-
- // Also populate the fast lookup map for direct imports
- let libExports = this.knownRootImports.get(config.libName)
- if (!libExports) {
- libExports = new Map()
- this.knownRootImports.set(config.libName, libExports)
- }
- libExports.set(config.rootExport, 'Root')
}),
)
@@ -234,15 +293,13 @@ export class ServerFnCompiler {
}
const info: ModuleInfo = {
- code,
id,
- ast,
bindings,
exports,
reExportAllSources,
}
this.moduleCache.set(id, info)
- return info
+ return { info, ast }
}
public invalidateModule(id: string) {
@@ -259,10 +316,10 @@ export class ServerFnCompiler {
isProviderFile: boolean
}) {
if (!this.initialized) {
- await this.init(id)
+ await this.init()
}
- const { bindings, ast } = this.ingestModule({ code, id })
- const candidates = this.collectCandidates(bindings)
+ const { info, ast } = this.ingestModule({ code, id })
+ const candidates = this.collectCandidates(info.bindings)
if (candidates.length === 0) {
// this hook will only be invoked if there is `.handler(` | `.server(` | `.client(` in the code,
// so not discovering a handler candidate is rather unlikely, but maybe possible?
@@ -378,10 +435,20 @@ export class ServerFnCompiler {
directive: this.options.directive,
isProviderFile,
})
- } else {
+ } else if (kind === 'Middleware') {
handleCreateMiddleware(candidate, {
env: this.options.env,
})
+ } else if (kind === 'IsomorphicFn') {
+ handleCreateIsomorphicFn(candidate, {
+ env: this.options.env,
+ })
+ } else {
+ // ServerOnlyFn or ClientOnlyFn
+ handleEnvOnlyFn(candidate, {
+ env: this.options.env,
+ kind,
+ })
}
}
@@ -399,13 +466,31 @@ export class ServerFnCompiler {
const candidates: Array = []
for (const binding of bindings.values()) {
- if (binding.type === 'var') {
- const candidate = isCandidateCallExpression(
+ if (binding.type === 'var' && t.isCallExpression(binding.init)) {
+ // Pattern 1: Method chain pattern (.handler(), .server(), etc.)
+ const methodChainCandidate = isCandidateCallExpression(
binding.init,
this.validLookupKinds,
)
- if (candidate) {
- candidates.push(candidate)
+ if (methodChainCandidate) {
+ candidates.push(methodChainCandidate)
+ continue
+ }
+
+ // Pattern 2: Direct call pattern
+ // Handles:
+ // - createServerOnlyFn(), createClientOnlyFn() (direct call kinds)
+ // - createIsomorphicFn() (root-as-candidate kinds)
+ // - TanStackStart.createServerOnlyFn() (namespace calls)
+ if (this.hasDirectCallKinds || this.hasRootAsCandidateKinds) {
+ if (
+ t.isIdentifier(binding.init.callee) ||
+ (t.isMemberExpression(binding.init.callee) &&
+ t.isIdentifier(binding.init.callee.property))
+ ) {
+ // Include as candidate - kind resolution will verify it's actually a known export
+ candidates.push(binding.init)
+ }
}
}
}
@@ -617,25 +702,40 @@ export class ServerFnCompiler {
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
const prop = callee.property.name
- if (
- this.validLookupKinds.has('ServerFn') &&
- LookupSetup['ServerFn'].candidateCallIdentifier.has(prop)
- ) {
- const base = await this.resolveExprKind(callee.object, fileId, visited)
- if (base === 'Root' || base === 'Builder') {
- return 'ServerFn'
- }
- return 'None'
- } else if (
- this.validLookupKinds.has('Middleware') &&
- LookupSetup['Middleware'].candidateCallIdentifier.has(prop)
- ) {
+ // Check if this property matches any method chain pattern
+ const possibleKinds = IdentifierToKinds.get(prop)
+ if (possibleKinds) {
+ // Resolve base expression ONCE and reuse for all pattern checks
const base = await this.resolveExprKind(callee.object, fileId, visited)
- if (base === 'Root' || base === 'Builder' || base === 'Middleware') {
- return 'Middleware'
+
+ // Check each possible kind that uses this identifier
+ for (const kind of possibleKinds) {
+ if (!this.validLookupKinds.has(kind)) continue
+
+ if (kind === 'ServerFn') {
+ if (base === 'Root' || base === 'Builder') {
+ return 'ServerFn'
+ }
+ } else if (kind === 'Middleware') {
+ if (
+ base === 'Root' ||
+ base === 'Builder' ||
+ base === 'Middleware'
+ ) {
+ return 'Middleware'
+ }
+ } else if (kind === 'IsomorphicFn') {
+ if (
+ base === 'Root' ||
+ base === 'Builder' ||
+ base === 'IsomorphicFn'
+ ) {
+ return 'IsomorphicFn'
+ }
+ }
}
- return 'None'
}
+
// Check if the object is a namespace import
if (t.isIdentifier(callee.object)) {
const info = await this.getModuleInfo(fileId)
@@ -704,10 +804,16 @@ function isCandidateCallExpression(
return undefined
}
- // Use pre-computed map for O(1) lookup instead of iterating over lookupKinds
- const kind = IdentifierToKind.get(callee.property.name)
- if (kind && lookupKinds.has(kind)) {
- return node
+ // Use pre-computed map for O(1) lookup
+ // IdentifierToKinds maps identifier -> Set to handle shared identifiers
+ const possibleKinds = IdentifierToKinds.get(callee.property.name)
+ if (possibleKinds) {
+ // Check if any of the possible kinds are in the valid lookup kinds
+ for (const kind of possibleKinds) {
+ if (lookupKinds.has(kind)) {
+ return node
+ }
+ }
}
return undefined
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts
new file mode 100644
index 00000000000..6e2d7c6be92
--- /dev/null
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts
@@ -0,0 +1,46 @@
+import * as t from '@babel/types'
+import type { RewriteCandidate } from './types'
+
+export function handleCreateIsomorphicFn(
+ candidate: RewriteCandidate,
+ opts: { env: 'client' | 'server' },
+) {
+ const { path, methodChain } = candidate
+
+ // Get the environment-specific call (.client() or .server())
+ const envCallInfo =
+ opts.env === 'client' ? methodChain.client : methodChain.server
+
+ // Check if we have any implementation at all
+ if (!methodChain.client && !methodChain.server) {
+ // No implementations provided - warn and replace with no-op
+ const variableId = path.parentPath.isVariableDeclarator()
+ ? path.parentPath.node.id
+ : null
+ console.warn(
+ 'createIsomorphicFn called without a client or server implementation!',
+ 'This will result in a no-op function.',
+ 'Variable name:',
+ t.isIdentifier(variableId) ? variableId.name : 'unknown',
+ )
+ path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([])))
+ return
+ }
+
+ if (!envCallInfo) {
+ // No implementation for this environment - replace with no-op
+ path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([])))
+ return
+ }
+
+ // Extract the function argument from the environment-specific call
+ const innerFn = envCallInfo.firstArgPath?.node
+
+ if (!t.isExpression(innerFn)) {
+ throw new Error(
+ `createIsomorphicFn().${opts.env}(func) must be called with a function!`,
+ )
+ }
+
+ path.replaceWith(innerFn)
+}
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts
index 8837e93c7ad..112d99f420d 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts
@@ -1,5 +1,5 @@
import * as t from '@babel/types'
-import { codeFrameError } from '../start-compiler-plugin/utils'
+import { codeFrameError } from './utils'
import type { RewriteCandidate } from './types'
/**
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts
new file mode 100644
index 00000000000..206d13eab48
--- /dev/null
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts
@@ -0,0 +1,45 @@
+import * as t from '@babel/types'
+import type { RewriteCandidate } from './types'
+import type { LookupKind } from './compiler'
+
+function capitalize(str: string) {
+ if (!str) return ''
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
+}
+
+export function handleEnvOnlyFn(
+ candidate: RewriteCandidate,
+ opts: { env: 'client' | 'server'; kind: LookupKind },
+) {
+ const { path } = candidate
+ const targetEnv = opts.kind === 'ClientOnlyFn' ? 'client' : 'server'
+
+ if (opts.env === targetEnv) {
+ // Matching environment - extract the inner function
+ const innerFn = path.node.arguments[0]
+
+ if (!t.isExpression(innerFn)) {
+ throw new Error(
+ `create${capitalize(targetEnv)}OnlyFn() must be called with a function!`,
+ )
+ }
+
+ path.replaceWith(innerFn)
+ } else {
+ // Wrong environment - replace with a function that throws an error
+ path.replaceWith(
+ t.arrowFunctionExpression(
+ [],
+ t.blockStatement([
+ t.throwStatement(
+ t.newExpression(t.identifier('Error'), [
+ t.stringLiteral(
+ `create${capitalize(targetEnv)}OnlyFn() functions can only be called on the ${targetEnv}!`,
+ ),
+ ]),
+ ),
+ ]),
+ ),
+ )
+ }
+}
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 eebb000487b..f19ffbda102 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,41 +1,74 @@
import { TRANSFORM_ID_REGEX } from '../constants'
import { ServerFnCompiler } from './compiler'
+import type { CompileStartFrameworkOptions } from '../types'
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 queryIndex = id.indexOf('?')
+ return queryIndex === -1 ? id : id.substring(0, queryIndex)
}
const LookupKindsPerEnv: Record<'client' | 'server', Set> = {
- client: new Set(['Middleware', 'ServerFn'] as const),
- server: new Set(['ServerFn'] as const),
+ client: new Set([
+ 'Middleware',
+ 'ServerFn',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ] as const),
+ server: new Set([
+ 'ServerFn',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ] as const),
}
const getLookupConfigurationsForEnv = (
env: 'client' | 'server',
framework: CompileStartFrameworkOptions,
): Array => {
- const createServerFnConfig: LookupConfig = {
- libName: `@tanstack/${framework}-start`,
- rootExport: 'createServerFn',
- }
+ // Common configs for all environments
+ const commonConfigs: Array = [
+ {
+ libName: `@tanstack/${framework}-start`,
+ rootExport: 'createServerFn',
+ kind: 'Root',
+ },
+ {
+ libName: `@tanstack/${framework}-start`,
+ rootExport: 'createIsomorphicFn',
+ kind: 'IsomorphicFn',
+ },
+ {
+ libName: `@tanstack/${framework}-start`,
+ rootExport: 'createServerOnlyFn',
+ kind: 'ServerOnlyFn',
+ },
+ {
+ libName: `@tanstack/${framework}-start`,
+ rootExport: 'createClientOnlyFn',
+ kind: 'ClientOnlyFn',
+ },
+ ]
+
if (env === 'client') {
return [
{
libName: `@tanstack/${framework}-start`,
rootExport: 'createMiddleware',
+ kind: 'Root',
},
{
libName: `@tanstack/${framework}-start`,
rootExport: 'createStart',
+ kind: 'Root',
},
-
- createServerFnConfig,
+ ...commonConfigs,
]
} else {
- return [createServerFnConfig]
+ return commonConfigs
}
}
const SERVER_FN_LOOKUP = 'server-fn-module-lookup'
@@ -44,6 +77,12 @@ function buildDirectiveSplitParam(directive: string) {
return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}`
}
+const commonTransformCodeFilter = [
+ /\.\s*handler\(/,
+ /createIsomorphicFn/,
+ /createServerOnlyFn/,
+ /createClientOnlyFn/,
+]
export function createServerFnPlugin(opts: {
framework: CompileStartFrameworkOptions
directive: string
@@ -56,11 +95,16 @@ export function createServerFnPlugin(opts: {
name: string
type: 'client' | 'server'
}): PluginOption {
- // in server environments, we don't transform middleware calls
+ // Code filter patterns for transform functions:
+ // - `.handler(` for createServerFn
+ // - `createMiddleware(` for middleware (client only)
+ // - `createIsomorphicFn` for isomorphic functions
+ // - `createServerOnlyFn` for server-only functions
+ // - `createClientOnlyFn` for client-only functions
const transformCodeFilter =
environment.type === 'client'
- ? [/\.\s*handler\(/, /\.\s*createMiddleware\(\)/]
- : [/\.\s*handler\(/]
+ ? [...commonTransformCodeFilter, /createMiddleware\s*\(/]
+ : commonTransformCodeFilter
return {
name: `tanstack-start-core::server-fn:${environment.name}`,
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/types.ts b/packages/start-plugin-core/src/create-server-fn-plugin/types.ts
index 598c11c8c72..02886dc9fbc 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/types.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/types.ts
@@ -25,14 +25,6 @@ export interface MethodChainPaths {
export type MethodChainKey = keyof MethodChainPaths
-export const METHOD_CHAIN_KEYS: ReadonlyArray = [
- 'middleware',
- 'inputValidator',
- 'handler',
- 'server',
- 'client',
-] as const
-
/**
* Information about a candidate that needs to be rewritten.
*/
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts b/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts
new file mode 100644
index 00000000000..68d60a4fb12
--- /dev/null
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts
@@ -0,0 +1,24 @@
+import { codeFrameColumns } from '@babel/code-frame'
+
+export function codeFrameError(
+ code: string,
+ loc: {
+ start: { line: number; column: number }
+ end: { line: number; column: number }
+ },
+ message: string,
+) {
+ const frame = codeFrameColumns(
+ code,
+ {
+ start: loc.start,
+ end: loc.end,
+ },
+ {
+ highlightCode: true,
+ message,
+ },
+ )
+
+ return new Error(frame)
+}
diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts
index 1a9a74045f1..cfca4a12161 100644
--- a/packages/start-plugin-core/src/plugin.ts
+++ b/packages/start-plugin-core/src/plugin.ts
@@ -6,7 +6,6 @@ import { crawlFrameworkPkgs } from 'vitefu'
import { join } from 'pathe'
import { escapePath } from 'tinyglobby'
import { startManifestPlugin } from './start-manifest-plugin/plugin'
-import { startCompilerPlugin } from './start-compiler-plugin/plugin'
import { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from './constants'
import { tanStackStartRouter } from './start-router-plugin/plugin'
import { loadEnvPlugin } from './load-env-plugin/plugin'
@@ -20,44 +19,17 @@ import {
} from './output-directory'
import { postServerBuild } from './post-server-build'
import { createServerFnPlugin } from './create-server-fn-plugin/plugin'
+import type {
+ GetConfigFn,
+ ResolvedStartConfig,
+ TanStackStartVitePluginCoreOptions,
+} from './types'
import type { ViteEnvironmentNames } from './constants'
import type {
TanStackStartInputConfig,
TanStackStartOutputConfig,
} from './schema'
import type { PluginOption } from 'vite'
-import type { CompileStartFrameworkOptions } from './start-compiler-plugin/compilers'
-
-export interface TanStackStartVitePluginCoreOptions {
- framework: CompileStartFrameworkOptions
- defaultEntryPaths: {
- client: string
- server: string
- start: string
- }
- serverFn?: {
- directive?: string
- ssr?: {
- getServerFnById?: string
- }
- providerEnv?: string
- }
-}
-
-export interface ResolvedStartConfig {
- root: string
- startFilePath: string | undefined
- routerFilePath: string
- srcDirectory: string
- viteAppBase: string
- serverFnProviderEnv: string
-}
-
-export type GetConfigFn = () => {
- startConfig: TanStackStartOutputConfig
- resolvedStartConfig: ResolvedStartConfig
- corePluginOpts: TanStackStartVitePluginCoreOptions
-}
function isFullUrl(str: string): boolean {
try {
@@ -431,7 +403,8 @@ export function TanStackStartVitePluginCore(
envName: serverFnProviderEnv,
},
}),
- startCompilerPlugin({ framework: corePluginOpts.framework, environments }),
+ // Note: startCompilerPlugin functionality (createIsomorphicFn, createServerOnlyFn, createClientOnlyFn)
+ // is now merged into createServerFnPlugin above
loadEnvPlugin(),
startManifestPlugin({
getClientBundle: () => getBundle(VITE_ENVIRONMENT_NAMES.client),
diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts
index 7dec32effda..235796a1290 100644
--- a/packages/start-plugin-core/src/schema.ts
+++ b/packages/start-plugin-core/src/schema.ts
@@ -1,7 +1,7 @@
import path from 'node:path'
import { z } from 'zod'
import { configSchema, getConfig } from '@tanstack/router-plugin'
-import type { TanStackStartVitePluginCoreOptions } from './plugin'
+import type { TanStackStartVitePluginCoreOptions } from './types'
const tsrConfig = configSchema
.omit({ autoCodeSplitting: true, target: true, verboseFileRoutes: true })
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts b/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts
deleted file mode 100644
index df80fbf1608..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import * as babel from '@babel/core'
-import * as t from '@babel/types'
-
-import {
- deadCodeElimination,
- findReferencedIdentifiers,
-} from 'babel-dead-code-elimination'
-import { generateFromAst, parseAst } from '@tanstack/router-utils'
-import { transformFuncs } from './constants'
-import { handleCreateIsomorphicFnCallExpression } from './isomorphicFn'
-import {
- handleCreateClientOnlyFnCallExpression,
- handleCreateServerOnlyFnCallExpression,
-} from './envOnly'
-import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils'
-
-export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue'
-
-type Identifiers = { [K in (typeof transformFuncs)[number]]: IdentifierConfig }
-
-export function compileStartOutputFactory(
- framework: CompileStartFrameworkOptions,
-) {
- return function compileStartOutput(opts: CompileOptions): GeneratorResult {
- const identifiers: Partial = {
- createServerOnlyFn: {
- name: 'createServerOnlyFn',
- handleCallExpression: handleCreateServerOnlyFnCallExpression,
- paths: [],
- },
- createClientOnlyFn: {
- name: 'createClientOnlyFn',
- handleCallExpression: handleCreateClientOnlyFnCallExpression,
- paths: [],
- },
- createIsomorphicFn: {
- name: 'createIsomorphicFn',
- handleCallExpression: handleCreateIsomorphicFnCallExpression,
- paths: [],
- },
- }
-
- const ast = parseAst(opts)
-
- const doDce = opts.dce ?? true
- // find referenced identifiers *before* we transform anything
- const refIdents = doDce ? findReferencedIdentifiers(ast) : undefined
-
- const validImportSources = [
- `@tanstack/${framework}-start`,
- '@tanstack/start-client-core',
- ]
- babel.traverse(ast, {
- Program: {
- enter(programPath) {
- programPath.traverse({
- ImportDeclaration: (path) => {
- if (!validImportSources.includes(path.node.source.value)) {
- return
- }
-
- // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/react-start';"
- path.node.specifiers.forEach((specifier) => {
- transformFuncs.forEach((identifierKey) => {
- const identifier = identifiers[identifierKey]
- if (!identifier) {
- return
- }
- if (
- specifier.type === 'ImportSpecifier' &&
- specifier.imported.type === 'Identifier'
- ) {
- if (specifier.imported.name === identifierKey) {
- identifier.name = specifier.local.name
- }
- }
-
- // handle namespace imports like "import * as TanStackStart from '@tanstack/react-start';"
- if (specifier.type === 'ImportNamespaceSpecifier') {
- identifier.name = `${specifier.local.name}.${identifierKey}`
- }
- })
- })
- },
- CallExpression: (path) => {
- transformFuncs.forEach((identifierKey) => {
- const identifier = identifiers[identifierKey]
- if (!identifier) {
- return
- }
- // Check to see if the call expression is a call to the
- // identifiers[identifierKey].name
- if (
- t.isIdentifier(path.node.callee) &&
- path.node.callee.name === identifier.name
- ) {
- // The identifier could be a call to the original function
- // in the source code. If this is case, we need to ignore it.
- // Check the scope to see if the identifier is a function declaration.
- // if it is, then we can ignore it.
-
- if (
- path.scope.getBinding(identifier.name)?.path.node.type ===
- 'FunctionDeclaration'
- ) {
- return
- }
-
- return identifier.paths.push(path)
- }
-
- // handle namespace imports like "import * as TanStackStart from '@tanstack/react-start';"
- // which are then called like "TanStackStart.createServerFn()"
- if (t.isMemberExpression(path.node.callee)) {
- if (
- t.isIdentifier(path.node.callee.object) &&
- t.isIdentifier(path.node.callee.property)
- ) {
- const callname = [
- path.node.callee.object.name,
- path.node.callee.property.name,
- ].join('.')
-
- if (callname === identifier.name) {
- identifier.paths.push(path)
- }
- }
- }
-
- return
- })
- },
- })
-
- transformFuncs.forEach((identifierKey) => {
- const identifier = identifiers[identifierKey]
- if (!identifier) {
- return
- }
- identifier.paths.forEach((path) => {
- identifier.handleCallExpression(
- path as babel.NodePath,
- opts,
- )
- })
- })
- },
- },
- })
-
- if (doDce) {
- deadCodeElimination(ast, refIdents)
- }
-
- return generateFromAst(ast, {
- sourceMaps: true,
- sourceFileName: opts.filename,
- filename: opts.filename,
- })
- }
-}
-
-export type CompileOptions = ParseAstOptions & {
- env: 'server' | 'client'
- dce?: boolean
- filename: string
-}
-
-export type IdentifierConfig = {
- name: string
- handleCallExpression: (
- path: babel.NodePath,
- opts: CompileOptions,
- ) => void
- paths: Array
-}
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/constants.ts b/packages/start-plugin-core/src/start-compiler-plugin/constants.ts
deleted file mode 100644
index d5416a2c03e..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/constants.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const transformFuncs = [
- 'createServerOnlyFn',
- 'createClientOnlyFn',
- 'createIsomorphicFn',
-] as const
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts b/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts
deleted file mode 100644
index c3c0a00f1cc..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import * as t from '@babel/types'
-import type * as babel from '@babel/core'
-
-import type { CompileOptions } from './compilers'
-
-function capitalize(str: string) {
- if (!str) return ''
- return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
-}
-
-function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') {
- return function envOnlyCallExpressionHandler(
- path: babel.NodePath,
- opts: CompileOptions,
- ) {
- // if (debug)
- // console.info(`Handling ${env}Only call expression:`, path.toString())
-
- const isEnvMatch =
- env === 'client' ? opts.env === 'client' : opts.env === 'server'
-
- if (isEnvMatch) {
- // extract the inner function from the call expression
- const innerInputExpression = path.node.arguments[0]
-
- if (!t.isExpression(innerInputExpression)) {
- throw new Error(
- `${env}Only() functions must be called with a function!`,
- )
- }
-
- path.replaceWith(innerInputExpression)
- return
- }
-
- // If we're on the wrong environment, replace the call expression
- // with a function that always throws an error.
- path.replaceWith(
- t.arrowFunctionExpression(
- [],
- t.blockStatement([
- t.throwStatement(
- t.newExpression(t.identifier('Error'), [
- t.stringLiteral(
- `create${capitalize(env)}OnlyFn() functions can only be called on the ${env}!`,
- ),
- ]),
- ),
- ]),
- ),
- )
- }
-}
-
-export const handleCreateServerOnlyFnCallExpression =
- buildEnvOnlyCallExpressionHandler('server')
-export const handleCreateClientOnlyFnCallExpression =
- buildEnvOnlyCallExpressionHandler('client')
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts b/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts
deleted file mode 100644
index b9b68e678ac..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import * as t from '@babel/types'
-import { getRootCallExpression } from './utils'
-import type * as babel from '@babel/core'
-
-import type { CompileOptions } from './compilers'
-
-export function handleCreateIsomorphicFnCallExpression(
- path: babel.NodePath,
- opts: CompileOptions,
-) {
- const rootCallExpression = getRootCallExpression(path)
-
- // if (debug)
- // console.info(
- // 'Handling createIsomorphicFn call expression:',
- // rootCallExpression.toString(),
- // )
-
- const callExpressionPaths = {
- client: null as babel.NodePath | null,
- server: null as babel.NodePath | null,
- }
-
- const validMethods = Object.keys(callExpressionPaths)
-
- rootCallExpression.traverse({
- MemberExpression(memberExpressionPath) {
- if (t.isIdentifier(memberExpressionPath.node.property)) {
- const name = memberExpressionPath.node.property
- .name as keyof typeof callExpressionPaths
-
- if (
- validMethods.includes(name) &&
- memberExpressionPath.parentPath.isCallExpression()
- ) {
- callExpressionPaths[name] = memberExpressionPath.parentPath
- }
- }
- },
- })
-
- if (
- validMethods.every(
- (method) =>
- !callExpressionPaths[method as keyof typeof callExpressionPaths],
- )
- ) {
- const variableId = rootCallExpression.parentPath.isVariableDeclarator()
- ? rootCallExpression.parentPath.node.id
- : null
- console.warn(
- 'createIsomorphicFn called without a client or server implementation!',
- 'This will result in a no-op function.',
- 'Variable name:',
- t.isIdentifier(variableId) ? variableId.name : 'unknown',
- )
- }
-
- const envCallExpression = callExpressionPaths[opts.env]
-
- if (!envCallExpression) {
- // if we don't have an implementation for this environment, default to a no-op
- rootCallExpression.replaceWith(
- t.arrowFunctionExpression([], t.blockStatement([])),
- )
- return
- }
-
- const innerInputExpression = envCallExpression.node.arguments[0]
-
- if (!t.isExpression(innerInputExpression)) {
- throw new Error(
- `createIsomorphicFn().${opts.env}(func) must be called with a function!`,
- )
- }
-
- rootCallExpression.replaceWith(innerInputExpression)
-}
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts
deleted file mode 100644
index 8d8ed6cd61e..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { fileURLToPath, pathToFileURL } from 'node:url'
-import { createRequire } from 'node:module'
-import { logDiff } from '@tanstack/router-utils'
-
-import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
-import { normalizePath } from 'vite'
-import path from 'pathe'
-import { makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils'
-import { TRANSFORM_ID_REGEX } from '../constants'
-import { compileStartOutputFactory } from './compilers'
-import { transformFuncs } from './constants'
-import type { Plugin, PluginOption } from 'vite'
-import type { CompileStartFrameworkOptions } from './compilers'
-
-const debug =
- process.env.TSR_VITE_DEBUG &&
- ['true', 'start-plugin'].includes(process.env.TSR_VITE_DEBUG)
-
-export type TanStackStartViteOptions = {
- globalMiddlewareEntry: string
-}
-
-const tokenRegex = new RegExp(transformFuncs.join('|'))
-
-const require = createRequire(import.meta.url)
-
-function resolveRuntimeFiles(opts: { package: string; files: Array }) {
- const pkgRoot = resolvePackage(opts.package)
- const basePath = path.join(pkgRoot, 'dist', 'esm')
- return opts.files.map((file) => normalizePath(path.join(basePath, file)))
-}
-
-function resolvePackage(packageName: string): string {
- const pkgRoot = path.dirname(require.resolve(packageName + '/package.json'))
- return pkgRoot
-}
-
-const transformFilter = {
- code: tokenRegex,
- id: {
- include: TRANSFORM_ID_REGEX,
- exclude: [
- VIRTUAL_MODULES.serverFnManifest,
- // N.B. the following files either just re-export or provide the runtime implementation of those functions
- // we do not want to include them in the transformation
- // however, those packages (especially start-client-core ATM) also USE these functions
- // (namely `createIsomorphicFn` in `packages/start-client-core/src/getRouterInstance.ts`) and thus need to be transformed
- ...makeIdFiltersToMatchWithQuery([
- ...resolveRuntimeFiles({
- package: '@tanstack/start-client-core',
- files: [
- 'index.js',
- 'createIsomorphicFn.js',
- 'envOnly.js',
- 'serverFnFetcher.js',
- 'createStart.js',
- 'createMiddleware.js',
- ],
- }),
- ...resolveRuntimeFiles({
- package: '@tanstack/start-server-core',
- files: ['index.js', 'server-functions-handler.js'],
- }),
- ]),
- ],
- },
-}
-
-export function startCompilerPlugin(opts: {
- framework: CompileStartFrameworkOptions
- environments: Array<{ name: string; type: 'client' | 'server' }>
-}): PluginOption {
- const compileStartOutput = compileStartOutputFactory(opts.framework)
-
- function perEnvCompilerPlugin(environment: {
- name: string
- type: 'client' | 'server'
- }): Plugin {
- return {
- name: `tanstack-start-core:compiler:${environment.name}`,
- enforce: 'pre',
- applyToEnvironment(env) {
- return env.name === environment.name
- },
- transform: {
- filter: transformFilter,
- handler(code, id) {
- const url = pathToFileURL(id)
- url.searchParams.delete('v')
- id = fileURLToPath(url).replace(/\\/g, '/')
-
- if (debug) console.info(`${environment.name} Compiling Start: `, id)
-
- const compiled = compileStartOutput({
- code,
- filename: id,
- env: environment.type,
- })
-
- if (debug) {
- logDiff(code, compiled.code)
- console.log('Output:\n', compiled.code + '\n\n')
- }
-
- return compiled
- },
- },
- }
- }
- return opts.environments.map(perEnvCompilerPlugin)
-}
diff --git a/packages/start-plugin-core/src/start-compiler-plugin/utils.ts b/packages/start-plugin-core/src/start-compiler-plugin/utils.ts
deleted file mode 100644
index dcfa6014a28..00000000000
--- a/packages/start-plugin-core/src/start-compiler-plugin/utils.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { codeFrameColumns } from '@babel/code-frame'
-import type * as t from '@babel/types'
-import type * as babel from '@babel/core'
-
-export function getRootCallExpression(path: babel.NodePath) {
- // Find the highest callExpression parent
- let rootCallExpression: babel.NodePath = path
-
- // Traverse up the chain of CallExpressions
- while (rootCallExpression.parentPath.isMemberExpression()) {
- const parent = rootCallExpression.parentPath
- if (parent.parentPath.isCallExpression()) {
- rootCallExpression = parent.parentPath
- }
- }
-
- return rootCallExpression
-}
-
-export function codeFrameError(
- code: string,
- loc: {
- start: { line: number; column: number }
- end: { line: number; column: number }
- },
- message: string,
-) {
- const frame = codeFrameColumns(
- code,
- {
- start: loc.start,
- end: loc.end,
- },
- {
- highlightCode: true,
- message,
- },
- )
-
- return new Error(frame)
-}
diff --git a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
index 50fc50ad47e..3f28bea375c 100644
--- a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
@@ -4,7 +4,7 @@ import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { tsrSplit } from '@tanstack/router-plugin'
import { resolveViteId } from '../utils'
import { ENTRY_POINTS } from '../constants'
-import type { GetConfigFn } from '../plugin'
+import type { GetConfigFn } from '../types'
import type { PluginOption, Rollup } from 'vite'
import type { Manifest, RouterManagedTag } from '@tanstack/router-core'
diff --git a/packages/start-plugin-core/src/start-router-plugin/plugin.ts b/packages/start-plugin-core/src/start-router-plugin/plugin.ts
index 4543eac384b..1536d19c91e 100644
--- a/packages/start-plugin-core/src/start-router-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/start-router-plugin/plugin.ts
@@ -10,6 +10,7 @@ import { routesManifestPlugin } from './generator-plugins/routes-manifest-plugin
import { prerenderRoutesPlugin } from './generator-plugins/prerender-routes-plugin'
import { pruneServerOnlySubtrees } from './pruneServerOnlySubtrees'
import { SERVER_PROP } from './constants'
+import type { GetConfigFn, TanStackStartVitePluginCoreOptions } from '../types'
import type {
Generator,
GeneratorPlugin,
@@ -17,7 +18,6 @@ import type {
} from '@tanstack/router-generator'
import type { DevEnvironment, Plugin, PluginOption } from 'vite'
import type { TanStackStartInputConfig } from '../schema'
-import type { GetConfigFn, TanStackStartVitePluginCoreOptions } from '../plugin'
function isServerOnlyNode(node: RouteNode | undefined) {
if (!node?.createFileRouteProps) {
diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts
new file mode 100644
index 00000000000..b1e3e6a0f19
--- /dev/null
+++ b/packages/start-plugin-core/src/types.ts
@@ -0,0 +1,34 @@
+import type { TanStackStartOutputConfig } from './schema'
+
+export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue'
+
+export interface TanStackStartVitePluginCoreOptions {
+ framework: CompileStartFrameworkOptions
+ defaultEntryPaths: {
+ client: string
+ server: string
+ start: string
+ }
+ serverFn?: {
+ directive?: string
+ ssr?: {
+ getServerFnById?: string
+ }
+ providerEnv?: string
+ }
+}
+
+export interface ResolvedStartConfig {
+ root: string
+ startFilePath: string | undefined
+ routerFilePath: string
+ srcDirectory: string
+ viteAppBase: string
+ serverFnProviderEnv: string
+}
+
+export type GetConfigFn = () => {
+ startConfig: TanStackStartOutputConfig
+ resolvedStartConfig: ResolvedStartConfig
+ corePluginOpts: TanStackStartVitePluginCoreOptions
+}
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts
index ef65d23d0d7..04d2946b063 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts
@@ -2,9 +2,38 @@ import { readFile, readdir } from 'node:fs/promises'
import path from 'node:path'
import { afterAll, describe, expect, test, vi } from 'vitest'
-import { compileStartOutputFactory } from '../../src/start-compiler-plugin/compilers'
+import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler'
-const compileStartOutput = compileStartOutputFactory('react')
+async function compile(opts: {
+ env: 'client' | 'server'
+ code: string
+ id: string
+}) {
+ const compiler = new ServerFnCompiler({
+ ...opts,
+ loadModule: async () => {
+ // do nothing in test
+ },
+ lookupKinds: new Set(['IsomorphicFn']),
+ lookupConfigurations: [
+ {
+ libName: `@tanstack/react-start`,
+ rootExport: 'createIsomorphicFn',
+ kind: 'IsomorphicFn',
+ },
+ ],
+ resolveId: async (id) => {
+ return id
+ },
+ directive: 'use server',
+ })
+ const result = await compiler.compile({
+ code: opts.code,
+ id: opts.id,
+ isProviderFile: false,
+ })
+ return result
+}
async function getFilenames() {
return await readdir(path.resolve(import.meta.dirname, './test-files'))
@@ -38,49 +67,48 @@ describe('createIsomorphicFn compiles correctly', async () => {
test.each(['client', 'server'] as const)(
`should compile for ${filename} %s`,
async (env) => {
- const compiledResult = compileStartOutput({
+ const compiledResult = await compile({
env,
code,
- filename,
- dce: false,
+ id: filename,
})
- await expect(compiledResult.code).toMatchFileSnapshot(
+ await expect(compiledResult!.code).toMatchFileSnapshot(
`./snapshots/${env}/${filename}`,
)
},
)
})
- test('should error if implementation not provided', () => {
- expect(() => {
- compileStartOutput({
+
+ test('should error if implementation not provided', async () => {
+ await expect(
+ compile({
env: 'client',
code: `
import { createIsomorphicFn } from '@tanstack/react-start'
const clientOnly = createIsomorphicFn().client()`,
- filename: 'no-fn.ts',
- dce: false,
- })
- }).toThrowError()
- expect(() => {
- compileStartOutput({
+ id: 'no-fn.ts',
+ }),
+ ).rejects.toThrowError()
+
+ await expect(
+ compile({
env: 'server',
code: `
import { createIsomorphicFn } from '@tanstack/react-start'
const serverOnly = createIsomorphicFn().server()`,
- filename: 'no-fn.ts',
- dce: false,
- })
- }).toThrowError()
+ id: 'no-fn.ts',
+ }),
+ ).rejects.toThrowError()
})
- test('should warn to console if no implementations provided', () => {
- compileStartOutput({
+
+ test('should warn to console if no implementations provided', async () => {
+ await compile({
env: 'client',
code: `
import { createIsomorphicFn } from '@tanstack/react-start'
const noImpl = createIsomorphicFn()`,
- filename: 'no-fn.ts',
- dce: false,
+ id: 'no-fn.ts',
})
expect(consoleSpy).toHaveBeenCalledWith(
noImplWarning,
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx
index 837195fb9c4..87eb2bba0ea 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx
@@ -1,12 +1,8 @@
-import { createIsomorphicFn } from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => {};
const clientOnlyFn = () => 'client';
const serverThenClientFn = () => 'client';
const clientThenServerFn = () => 'client';
-function abstractedServerFn() {
- return 'server';
-}
const serverOnlyFnAbstracted = () => {};
function abstractedClientFn() {
return 'client';
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx
index 4f0230416dc..87eb2bba0ea 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx
@@ -1,12 +1,8 @@
-import { createIsomorphicFn as isomorphicFn } from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => {};
const clientOnlyFn = () => 'client';
const serverThenClientFn = () => 'client';
const clientThenServerFn = () => 'client';
-function abstractedServerFn() {
- return 'server';
-}
const serverOnlyFnAbstracted = () => {};
function abstractedClientFn() {
return 'client';
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx
index 296ccdbe24b..87eb2bba0ea 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx
@@ -1,12 +1,8 @@
-import * as TanStackStart from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => {};
const clientOnlyFn = () => 'client';
const serverThenClientFn = () => 'client';
const clientThenServerFn = () => 'client';
-function abstractedServerFn() {
- return 'server';
-}
const serverOnlyFnAbstracted = () => {};
function abstractedClientFn() {
return 'client';
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx
index 1656889e535..f0b7d12af45 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx
@@ -1,4 +1,3 @@
-import { createIsomorphicFn } from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => 'server';
const clientOnlyFn = () => {};
@@ -8,9 +7,6 @@ function abstractedServerFn() {
return 'server';
}
const serverOnlyFnAbstracted = abstractedServerFn;
-function abstractedClientFn() {
- return 'client';
-}
const clientOnlyFnAbstracted = () => {};
const serverThenClientFnAbstracted = abstractedServerFn;
const clientThenServerFnAbstracted = abstractedServerFn;
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx
index a46f061a451..f0b7d12af45 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx
@@ -1,4 +1,3 @@
-import { createIsomorphicFn as isomorphicFn } from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => 'server';
const clientOnlyFn = () => {};
@@ -8,9 +7,6 @@ function abstractedServerFn() {
return 'server';
}
const serverOnlyFnAbstracted = abstractedServerFn;
-function abstractedClientFn() {
- return 'client';
-}
const clientOnlyFnAbstracted = () => {};
const serverThenClientFnAbstracted = abstractedServerFn;
const clientThenServerFnAbstracted = abstractedServerFn;
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx
index dc3ad9af048..f0b7d12af45 100644
--- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx
@@ -1,4 +1,3 @@
-import * as TanStackStart from '@tanstack/react-start';
const noImpl = () => {};
const serverOnlyFn = () => 'server';
const clientOnlyFn = () => {};
@@ -8,9 +7,6 @@ function abstractedServerFn() {
return 'server';
}
const serverOnlyFnAbstracted = abstractedServerFn;
-function abstractedClientFn() {
- return 'client';
-}
const clientOnlyFnAbstracted = () => {};
const serverThenClientFnAbstracted = abstractedServerFn;
const clientThenServerFnAbstracted = abstractedServerFn;
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts
similarity index 90%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts
rename to packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts
index e26d3abeda5..2133e31cfa5 100644
--- a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts
+++ b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts
@@ -14,7 +14,7 @@ async function compile(opts: {
}) {
const compiler = new ServerFnCompiler({
...opts,
- loadModule: async (id) => {
+ loadModule: async () => {
// do nothing in test
},
lookupKinds: new Set(['Middleware']),
@@ -22,10 +22,12 @@ async function compile(opts: {
{
libName: `@tanstack/react-start`,
rootExport: 'createMiddleware',
+ kind: 'Root',
},
{
libName: `@tanstack/react-start`,
rootExport: 'createStart',
+ kind: 'Root',
},
],
resolveId: async (id) => {
@@ -77,6 +79,7 @@ describe('createMiddleware compiles correctly', async () => {
{
libName: '@tanstack/react-start',
rootExport: 'createMiddleware',
+ kind: 'Root',
},
],
resolveId: resolveIdMock,
@@ -92,11 +95,9 @@ describe('createMiddleware compiles correctly', async () => {
// resolveId should only be called once during init() for the library itself
// It should NOT be called again to resolve the import binding because
// the fast path uses knownRootImports map for O(1) lookup
+ // Note: init() now resolves from project root, not from a specific file
expect(resolveIdMock).toHaveBeenCalledTimes(1)
- expect(resolveIdMock).toHaveBeenCalledWith(
- '@tanstack/react-start',
- 'test.ts',
- )
+ expect(resolveIdMock).toHaveBeenCalledWith('@tanstack/react-start')
})
test('should use slow path for factory pattern (resolveId called for import resolution)', async () => {
@@ -128,6 +129,7 @@ describe('createMiddleware compiles correctly', async () => {
{
libName: '@tanstack/react-start',
rootExport: 'createMiddleware',
+ kind: 'Root',
},
],
resolveId: resolveIdMock,
@@ -141,17 +143,13 @@ describe('createMiddleware compiles correctly', async () => {
})
// resolveId should be called exactly twice:
- // 1. Once during init() for '@tanstack/react-start'
+ // 1. Once during init() for '@tanstack/react-start' (no importer - resolved from project root)
// 2. Once to resolve './factory' import (slow path - not in knownRootImports)
//
// Note: The factory module's import from '@tanstack/react-start' ALSO uses
// the fast path (knownRootImports), so no additional resolveId call is needed there.
expect(resolveIdMock).toHaveBeenCalledTimes(2)
- expect(resolveIdMock).toHaveBeenNthCalledWith(
- 1,
- '@tanstack/react-start',
- 'test.ts',
- )
+ expect(resolveIdMock).toHaveBeenNthCalledWith(1, '@tanstack/react-start')
expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts')
})
})
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/create-function-middleware.ts b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/create-function-middleware.ts
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/create-function-middleware.ts
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/create-function-middleware.ts
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructured.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructured.tsx
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructuredRename.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructuredRename.tsx
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareStarImport.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareStarImport.tsx
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareValidator.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareValidator.tsx
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createStart.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createStart.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createStart.tsx
rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createStart.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/create-function-middleware.ts b/packages/start-plugin-core/tests/createMiddleware/test-files/create-function-middleware.ts
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/create-function-middleware.ts
rename to packages/start-plugin-core/tests/createMiddleware/test-files/create-function-middleware.ts
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructured.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructured.tsx
rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructuredRename.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructuredRename.tsx
rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareStarImport.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareStarImport.tsx
rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareValidator.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareValidator.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareValidator.tsx
rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareValidator.tsx
diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createStart.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createStart.tsx
similarity index 100%
rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createStart.tsx
rename to packages/start-plugin-core/tests/createMiddleware/test-files/createStart.tsx
diff --git a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts
index 758a61a94eb..f6161ccfaa9 100644
--- a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts
+++ b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts
@@ -23,6 +23,7 @@ async function compile(opts: {
{
libName: `@tanstack/react-start`,
rootExport: 'createServerFn',
+ kind: 'Root',
},
],
resolveId: async (id) => {
@@ -232,6 +233,7 @@ describe('createServerFn compiles correctly', async () => {
{
libName: '@tanstack/react-start',
rootExport: 'createServerFn',
+ kind: 'Root',
},
],
resolveId: resolveIdMock,
@@ -248,10 +250,7 @@ describe('createServerFn compiles correctly', async () => {
// It should NOT be called again to resolve the import binding because
// the fast path uses knownRootImports map for O(1) lookup
expect(resolveIdMock).toHaveBeenCalledTimes(1)
- expect(resolveIdMock).toHaveBeenCalledWith(
- '@tanstack/react-start',
- 'test.ts',
- )
+ expect(resolveIdMock).toHaveBeenCalledWith('@tanstack/react-start')
})
test('should use slow path for factory pattern (resolveId called for import resolution)', async () => {
@@ -283,6 +282,7 @@ describe('createServerFn compiles correctly', async () => {
{
libName: '@tanstack/react-start',
rootExport: 'createServerFn',
+ kind: 'Root',
},
],
resolveId: resolveIdMock,
@@ -302,11 +302,7 @@ describe('createServerFn compiles correctly', async () => {
// Note: The factory module's import from '@tanstack/react-start' ALSO uses
// the fast path (knownRootImports), so no additional resolveId call is needed there.
expect(resolveIdMock).toHaveBeenCalledTimes(2)
- expect(resolveIdMock).toHaveBeenNthCalledWith(
- 1,
- '@tanstack/react-start',
- 'test.ts',
- )
+ expect(resolveIdMock).toHaveBeenNthCalledWith(1, '@tanstack/react-start')
expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts')
})
})
diff --git a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts
index d68a546fb91..f3818234ab4 100644
--- a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts
+++ b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts
@@ -2,9 +2,43 @@ import { readFile, readdir } from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
-import { compileStartOutputFactory } from '../../src/start-compiler-plugin/compilers'
+import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler'
-const compileStartOutput = compileStartOutputFactory('react')
+async function compile(opts: {
+ env: 'client' | 'server'
+ code: string
+ id: string
+}) {
+ const compiler = new ServerFnCompiler({
+ ...opts,
+ loadModule: async () => {
+ // do nothing in test
+ },
+ lookupKinds: new Set(['ServerOnlyFn', 'ClientOnlyFn']),
+ lookupConfigurations: [
+ {
+ libName: `@tanstack/react-start`,
+ rootExport: 'createServerOnlyFn',
+ kind: 'ServerOnlyFn',
+ },
+ {
+ libName: `@tanstack/react-start`,
+ rootExport: 'createClientOnlyFn',
+ kind: 'ClientOnlyFn',
+ },
+ ],
+ resolveId: async (id) => {
+ return id
+ },
+ directive: 'use server',
+ })
+ const result = await compiler.compile({
+ code: opts.code,
+ id: opts.id,
+ isProviderFile: false,
+ })
+ return result
+}
async function getFilenames() {
return await readdir(path.resolve(import.meta.dirname, './test-files'))
@@ -22,39 +56,38 @@ describe('envOnly functions compile correctly', async () => {
test.each(['client', 'server'] as const)(
`should compile for ${filename} %s`,
async (env) => {
- const compiledResult = compileStartOutput({
+ const compiledResult = await compile({
env,
code,
- filename,
- dce: false,
+ id: filename,
})
- await expect(compiledResult.code).toMatchFileSnapshot(
+ await expect(compiledResult!.code).toMatchFileSnapshot(
`./snapshots/${env}/${filename}`,
)
},
)
})
- test('should error if implementation not provided', () => {
- expect(() => {
- compileStartOutput({
+
+ test('should error if implementation not provided', async () => {
+ await expect(
+ compile({
env: 'client',
code: `
import { createClientOnlyFn } from '@tanstack/react-start'
const fn = createClientOnlyFn()`,
- filename: 'no-fn.ts',
- dce: false,
- })
- }).toThrowError()
- expect(() => {
- compileStartOutput({
+ id: 'no-fn.ts',
+ }),
+ ).rejects.toThrowError()
+
+ await expect(
+ compile({
env: 'server',
code: `
import { createServerOnlyFn } from '@tanstack/react-start'
const fn = createServerOnlyFn()`,
- filename: 'no-fn.ts',
- dce: false,
- })
- }).toThrowError()
+ id: 'no-fn.ts',
+ }),
+ ).rejects.toThrowError()
})
})
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx
index d94dcb06dd5..d56ad1f374f 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx
@@ -1,4 +1,3 @@
-import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start';
const serverFunc = () => {
throw new Error("createServerOnlyFn() functions can only be called on the server!");
};
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx
index c3c0e21f6a0..d56ad1f374f 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx
@@ -1,4 +1,3 @@
-import { createServerOnlyFn as serverFn, createClientOnlyFn as clientFn } from '@tanstack/react-start';
const serverFunc = () => {
throw new Error("createServerOnlyFn() functions can only be called on the server!");
};
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx
index adafc67ab16..d56ad1f374f 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx
@@ -1,4 +1,3 @@
-import * as TanstackStart from '@tanstack/react-start';
const serverFunc = () => {
throw new Error("createServerOnlyFn() functions can only be called on the server!");
};
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx
index 7feb328db6b..5373578295e 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx
@@ -1,4 +1,3 @@
-import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start';
const serverFunc = () => 'server';
const clientFunc = () => {
throw new Error("createClientOnlyFn() functions can only be called on the client!");
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx
index 8d8bdac72ce..5373578295e 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx
@@ -1,4 +1,3 @@
-import { createServerOnlyFn as serverFn, createClientOnlyFn as clientFn } from '@tanstack/react-start';
const serverFunc = () => 'server';
const clientFunc = () => {
throw new Error("createClientOnlyFn() functions can only be called on the client!");
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx
index ce13e851258..5373578295e 100644
--- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx
@@ -1,4 +1,3 @@
-import * as TanstackStart from '@tanstack/react-start';
const serverFunc = () => 'server';
const clientFunc = () => {
throw new Error("createClientOnlyFn() functions can only be called on the client!");
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fffbd8ea37e..08f4c4a642e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10167,7 +10167,7 @@ importers:
devDependencies:
'@netlify/vite-plugin-tanstack-start':
specifier: ^1.1.4
- version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))
+ version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(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))
@@ -11780,6 +11780,9 @@ importers:
'@tanstack/router-core':
specifier: workspace:*
version: link:../router-core
+ '@tanstack/start-fn-stubs':
+ specifier: workspace:*
+ version: link:../start-fn-stubs
'@tanstack/start-storage-context':
specifier: workspace:*
version: link:../start-storage-context
@@ -11793,6 +11796,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
+ packages/start-fn-stubs: {}
+
packages/start-plugin-core:
dependencies:
'@babel/code-frame':
@@ -26390,13 +26395,13 @@ snapshots:
uuid: 11.1.0
write-file-atomic: 5.0.1
- '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+ '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/config': 23.2.0
'@netlify/dev-utils': 4.3.0
'@netlify/edge-functions-dev': 1.0.0
- '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+ '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
'@netlify/headers': 2.1.0
'@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)
'@netlify/redirects': 3.1.0
@@ -26464,12 +26469,12 @@ snapshots:
dependencies:
'@netlify/types': 2.1.0
- '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+ '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/dev-utils': 4.3.0
'@netlify/functions': 5.0.0
- '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+ '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -26559,9 +26564,9 @@ snapshots:
'@netlify/types@2.1.0': {}
- '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))':
+ '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))':
dependencies:
- '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))
+ '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))
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)
optionalDependencies:
'@tanstack/solid-start': link:packages/solid-start
@@ -26589,9 +26594,9 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))':
+ '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))':
dependencies:
- '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+ '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
'@netlify/dev-utils': 4.3.0
dedent: 1.7.0(babel-plugin-macros@3.1.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)
@@ -26619,13 +26624,13 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+ '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.7.1
- '@vercel/nft': 0.29.4(rollup@4.52.5)
+ '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -29726,7 +29731,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.29.4(rollup@4.52.5)':
+ '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)
diff --git a/scripts/publish.js b/scripts/publish.js
index 2760c5a87c4..c855c56b3b6 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -144,6 +144,10 @@ await publish({
name: '@tanstack/start-storage-context',
packageDir: 'packages/start-storage-context',
},
+ {
+ name: '@tanstack/start-fn-stubs',
+ packageDir: 'packages/start-fn-stubs',
+ },
{
name: '@tanstack/react-start',
packageDir: 'packages/react-start',