Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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/'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -679,6 +700,7 @@ const rootRouteChildren: RootRouteChildren = {
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
MiddlewareServerImportMiddlewareRoute: MiddlewareServerImportMiddlewareRoute,
RedirectTestSsrTargetRoute: RedirectTestSsrTargetRoute,
RedirectTestTargetRoute: RedirectTestTargetRoute,
AbortSignalIndexRoute: AbortSignalIndexRoute,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ function RouteComponent() {
Request Middleware in combination with server function
</Route.Link>
</li>
<li>
<Route.Link
to="./server-import-middleware"
data-testid="server-import-middleware-link"
>
Server imports in middleware are stripped from client build
</Route.Link>
</li>
</ul>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div>
<h2>Server Import in Middleware Test</h2>
<p>
This test verifies that server-only imports (getRequestHeaders) inside
createMiddleware().server() are properly stripped from the client build.
</p>
<button
onClick={handleClick}
data-testid="test-server-import-middleware-btn"
>
Call server function with middleware
</button>
{result && (
<div data-testid="server-import-middleware-result">
{result.testHeader}
</div>
)}
{error && (
<div data-testid="server-import-middleware-error">Error: {error}</div>
)}
</div>
)
}
20 changes: 20 additions & 0 deletions e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
3 changes: 3 additions & 0 deletions labeler-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*'
Expand Down
1 change: 1 addition & 0 deletions packages/start-client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/start-client-core/src/getGlobalStartContext.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
2 changes: 1 addition & 1 deletion packages/start-client-core/src/getRouterInstance.ts
Original file line number Diff line number Diff line change
@@ -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<RegisteredRouter> =
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion packages/start-client-core/src/getStartOptions.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
5 changes: 3 additions & 2 deletions packages/start-client-core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions packages/start-fn-stubs/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-check

import rootConfig from '../../eslint.config.js'

export default [
...rootConfig,
{
files: ['**/*.{ts,tsx}'],
},
{
plugins: {},
rules: {},
},
]
59 changes: 59 additions & 0 deletions packages/start-fn-stubs/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
9 changes: 9 additions & 0 deletions packages/start-fn-stubs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
createIsomorphicFn,
type IsomorphicFn,
type ServerOnlyFn,
type ClientOnlyFn,
type IsomorphicFnBase,
} from './createIsomorphicFn'

export { createServerOnlyFn, createClientOnlyFn } from './envOnly'
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expectTypeOf, test } from 'vitest'
import { createClientOnlyFn, createServerOnlyFn } from '../envOnly'
import { createClientOnlyFn, createServerOnlyFn } from '../src/envOnly'

const inputFn = () => 'output'

Expand Down
7 changes: 7 additions & 0 deletions packages/start-fn-stubs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src", "vite.config.ts"]
}
Loading
Loading