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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { createBarServerFn } from './createBarServerFn'
import { createFooServerFn } from './createFooServerFn'
import { createFakeFn } from './createFakeFn'
// Test re-export syntax: `export { foo } from './module'`
import { reexportFactory } from './reexportIndex'
// Test star re-export syntax: `export * from './module'`
import { starReexportFactory } from './starReexportIndex'

export const fooFn = createFooServerFn().handler(({ context }) => {
return {
Expand Down Expand Up @@ -91,3 +95,23 @@ export const composedFn = composeFactory()
context,
}
})

// Test that re-exported factories (using `export { foo } from './module'`) work correctly
// The middleware from reexportFactory should execute and add { reexport: 'reexport-middleware-executed' } to context
export const reexportedFactoryFn = reexportFactory().handler(({ context }) => {
return {
name: 'reexportedFactoryFn',
context,
}
})

// Test that star re-exported factories (using `export * from './module'`) work correctly
// The middleware from starReexportFactory should execute and add { starReexport: 'star-reexport-middleware-executed' } to context
export const starReexportedFactoryFn = starReexportFactory().handler(
({ context }) => {
return {
name: 'starReexportedFactoryFn',
context,
}
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file tests re-exporting a factory function from another module
export { reexportFactory } from './reexportWrapper'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createMiddleware, createServerFn } from '@tanstack/react-start'

const reexportMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
console.log('reexport middleware triggered')
return next({
context: { reexport: 'reexport-middleware-executed' } as const,
})
},
)

export const reexportFactory = createServerFn().middleware([reexportMiddleware])
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file tests re-exporting a factory function from another module using the star syntax
export * from './starReexportWrapper'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createMiddleware, createServerFn } from '@tanstack/react-start'

const starReexportMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
console.log('star reexport middleware triggered')
return next({
context: { starReexport: 'star-reexport-middleware-executed' } as const,
})
},
)

export const starReexportFactory = createServerFn().middleware([
starReexportMiddleware,
])
22 changes: 22 additions & 0 deletions e2e/react-start/server-functions/src/routes/factory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
fooFnPOST,
localFn,
localFnPOST,
reexportedFactoryFn,
starReexportedFactoryFn,
} from './-functions/functions'

export const Route = createFileRoute('/factory/')({
Expand Down Expand Up @@ -130,6 +132,26 @@ const functions = {
window,
},
},
// Test that re-exported factories (using `export { foo } from './module'`) work correctly
// The middleware from reexportFactory should execute and add { reexport: 'reexport-middleware-executed' } to context
reexportedFactoryFn: {
fn: reexportedFactoryFn,
type: 'serverFn',
expected: {
name: 'reexportedFactoryFn',
context: { reexport: 'reexport-middleware-executed' },
},
},
// Test that star re-exported factories (using `export * from './module'`) work correctly
// The middleware from starReexportFactory should execute and add { starReexport: 'star-reexport-middleware-executed' } to context
starReexportedFactoryFn: {
fn: starReexportedFactoryFn,
type: 'serverFn',
expected: {
name: 'starReexportedFactoryFn',
context: { starReexport: 'star-reexport-middleware-executed' },
},
},
} satisfies Record<string, TestCase>

interface TestCase {
Expand Down
48 changes: 48 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 @@ -543,3 +543,51 @@ test('redirect in server function called in query during SSR', async ({
await expect(page.getByTestId('redirect-target-ssr')).toBeVisible()
expect(page.url()).toContain('/redirect-test-ssr/target')
})

test('re-exported server function factory middleware executes correctly', async ({
page,
}) => {
// This test specifically verifies that when a server function factory is re-exported
// using `export { foo } from './module'` syntax, the middleware still executes.
// Previously, this syntax caused middleware to be silently skipped.
await page.goto('/factory')

await expect(page.getByTestId('factory-route-component')).toBeInViewport()

// Click the button for the re-exported factory function
await page.getByTestId('btn-fn-reexportedFactoryFn').click()

// Wait for the result
await expect(page.getByTestId('fn-result-reexportedFactoryFn')).toContainText(
'reexport-middleware-executed',
)

// Verify the full context was returned (middleware executed)
await expect(
page.getByTestId('fn-comparison-reexportedFactoryFn'),
).toContainText('equal')
})

test('star re-exported server function factory middleware executes correctly', async ({
page,
}) => {
// This test specifically verifies that when a server function factory is re-exported
// using `export * from './module'` syntax, the middleware still executes.
// Previously, this syntax caused middleware to be silently skipped.
await page.goto('/factory')

await expect(page.getByTestId('factory-route-component')).toBeInViewport()

// Click the button for the star re-exported factory function
await page.getByTestId('btn-fn-starReexportedFactoryFn').click()

// Wait for the result
await expect(
page.getByTestId('fn-result-starReexportedFactoryFn'),
).toContainText('star-reexport-middleware-executed')

// Verify the full context was returned (middleware executed)
await expect(
page.getByTestId('fn-comparison-starReexportedFactoryFn'),
).toContainText('equal')
})
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ interface ModuleInfo {
ast: ReturnType<typeof parseAst>
bindings: Map<string, Binding>
exports: Map<string, ExportEntry>
// Track `export * from './module'` declarations for re-export resolution
reExportAllSources: Array<string>
}

export class ServerFnCompiler {
Expand Down Expand Up @@ -86,6 +88,7 @@ export class ServerFnCompiler {
exports: new Map(),
code: '',
id: libId,
reExportAllSources: [],
}
this.moduleCache.set(libId, rootModule)
}
Expand Down Expand Up @@ -116,6 +119,7 @@ export class ServerFnCompiler {

const bindings = new Map<string, Binding>()
const exports = new Map<string, ExportEntry>()
const reExportAllSources: Array<string> = []

// we are only interested in top-level bindings, hence we don't traverse the AST
// instead we only iterate over the program body
Expand Down Expand Up @@ -178,6 +182,16 @@ export class ServerFnCompiler {
? sp.exported.name
: sp.exported.value
exports.set(exported, { tag: 'Normal', name: local })

// When re-exporting from another module (export { foo } from './module'),
// create an import binding so the server function can be resolved
if (node.source) {
bindings.set(local, {
type: 'import',
source: node.source.value,
importedName: local,
})
}
}
}
} else if (t.isExportDefaultDeclaration(node)) {
Expand All @@ -189,10 +203,21 @@ export class ServerFnCompiler {
bindings.set(synth, { type: 'var', init: d as t.Expression })
exports.set('default', { tag: 'Default', name: synth })
}
} else if (t.isExportAllDeclaration(node)) {
// Handle `export * from './module'` syntax
// Track the source so we can look up exports from it when needed
reExportAllSources.push(node.source.value)
}
}

const info: ModuleInfo = { code, id, ast, bindings, exports }
const info: ModuleInfo = {
code,
id,
ast,
bindings,
exports,
reExportAllSources,
}
this.moduleCache.set(id, info)
return info
}
Expand Down Expand Up @@ -343,7 +368,43 @@ export class ServerFnCompiler {

const importedModule = await this.getModuleInfo(target)

// Try to find the export in the module's direct exports
const moduleExport = importedModule.exports.get(binding.importedName)

// If not found directly, check re-export-all sources (`export * from './module'`)
if (!moduleExport && importedModule.reExportAllSources.length > 0) {
for (const reExportSource of importedModule.reExportAllSources) {
const reExportTarget = await this.options.resolveId(
reExportSource,
importedModule.id,
)
if (reExportTarget) {
const reExportModule = await this.getModuleInfo(reExportTarget)
const reExportEntry = reExportModule.exports.get(
binding.importedName,
)
if (reExportEntry) {
// Found the export in a re-exported module, resolve from there
const reExportBinding = reExportModule.bindings.get(
reExportEntry.name,
)
if (reExportBinding) {
if (reExportBinding.resolvedKind) {
return reExportBinding.resolvedKind
}
const resolvedKind = await this.resolveBindingKind(
reExportBinding,
reExportModule.id,
visited,
)
reExportBinding.resolvedKind = resolvedKind
return resolvedKind
}
}
}
}
}

if (!moduleExport) {
return 'None'
}
Expand Down
Loading