Skip to content

Commit 0348283

Browse files
fix: properly track re-exports of server function factories
fixes #6029
1 parent a727225 commit 0348283

8 files changed

Lines changed: 186 additions & 1 deletion

File tree

e2e/react-start/server-functions/src/routes/factory/-functions/functions.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { createMiddleware, createServerFn } from '@tanstack/react-start'
22
import { createBarServerFn } from './createBarServerFn'
33
import { createFooServerFn } from './createFooServerFn'
44
import { createFakeFn } from './createFakeFn'
5+
// Test re-export syntax: `export { foo } from './module'`
6+
import { reexportFactory } from './reexportIndex'
7+
// Test star re-export syntax: `export * from './module'`
8+
import { starReexportFactory } from './starReexportIndex'
59

610
export const fooFn = createFooServerFn().handler(({ context }) => {
711
return {
@@ -91,3 +95,23 @@ export const composedFn = composeFactory()
9195
context,
9296
}
9397
})
98+
99+
// Test that re-exported factories (using `export { foo } from './module'`) work correctly
100+
// The middleware from reexportFactory should execute and add { reexport: 'reexport-middleware-executed' } to context
101+
export const reexportedFactoryFn = reexportFactory().handler(({ context }) => {
102+
return {
103+
name: 'reexportedFactoryFn',
104+
context,
105+
}
106+
})
107+
108+
// Test that star re-exported factories (using `export * from './module'`) work correctly
109+
// The middleware from starReexportFactory should execute and add { starReexport: 'star-reexport-middleware-executed' } to context
110+
export const starReexportedFactoryFn = starReexportFactory().handler(
111+
({ context }) => {
112+
return {
113+
name: 'starReexportedFactoryFn',
114+
context,
115+
}
116+
},
117+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file tests re-exporting a factory function from another module
2+
export { reexportFactory } from './reexportWrapper'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
2+
3+
const reexportMiddleware = createMiddleware({ type: 'function' }).server(
4+
({ next }) => {
5+
console.log('reexport middleware triggered')
6+
return next({
7+
context: { reexport: 'reexport-middleware-executed' } as const,
8+
})
9+
},
10+
)
11+
12+
export const reexportFactory = createServerFn().middleware([reexportMiddleware])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file tests re-exporting a factory function from another module using the star syntax
2+
export * from './starReexportWrapper'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
2+
3+
const starReexportMiddleware = createMiddleware({ type: 'function' }).server(
4+
({ next }) => {
5+
console.log('star reexport middleware triggered')
6+
return next({
7+
context: { starReexport: 'star-reexport-middleware-executed' } as const,
8+
})
9+
},
10+
)
11+
12+
export const starReexportFactory = createServerFn().middleware([
13+
starReexportMiddleware,
14+
])

e2e/react-start/server-functions/src/routes/factory/index.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
fooFnPOST,
1313
localFn,
1414
localFnPOST,
15+
reexportedFactoryFn,
16+
starReexportedFactoryFn,
1517
} from './-functions/functions'
1618

1719
export const Route = createFileRoute('/factory/')({
@@ -130,6 +132,26 @@ const functions = {
130132
window,
131133
},
132134
},
135+
// Test that re-exported factories (using `export { foo } from './module'`) work correctly
136+
// The middleware from reexportFactory should execute and add { reexport: 'reexport-middleware-executed' } to context
137+
reexportedFactoryFn: {
138+
fn: reexportedFactoryFn,
139+
type: 'serverFn',
140+
expected: {
141+
name: 'reexportedFactoryFn',
142+
context: { reexport: 'reexport-middleware-executed' },
143+
},
144+
},
145+
// Test that star re-exported factories (using `export * from './module'`) work correctly
146+
// The middleware from starReexportFactory should execute and add { starReexport: 'star-reexport-middleware-executed' } to context
147+
starReexportedFactoryFn: {
148+
fn: starReexportedFactoryFn,
149+
type: 'serverFn',
150+
expected: {
151+
name: 'starReexportedFactoryFn',
152+
context: { starReexport: 'star-reexport-middleware-executed' },
153+
},
154+
},
133155
} satisfies Record<string, TestCase>
134156

135157
interface TestCase {

e2e/react-start/server-functions/tests/server-functions.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,3 +543,51 @@ test('redirect in server function called in query during SSR', async ({
543543
await expect(page.getByTestId('redirect-target-ssr')).toBeVisible()
544544
expect(page.url()).toContain('/redirect-test-ssr/target')
545545
})
546+
547+
test('re-exported server function factory middleware executes correctly', async ({
548+
page,
549+
}) => {
550+
// This test specifically verifies that when a server function factory is re-exported
551+
// using `export { foo } from './module'` syntax, the middleware still executes.
552+
// Previously, this syntax caused middleware to be silently skipped.
553+
await page.goto('/factory')
554+
555+
await expect(page.getByTestId('factory-route-component')).toBeInViewport()
556+
557+
// Click the button for the re-exported factory function
558+
await page.getByTestId('btn-fn-reexportedFactoryFn').click()
559+
560+
// Wait for the result
561+
await expect(page.getByTestId('fn-result-reexportedFactoryFn')).toContainText(
562+
'reexport-middleware-executed',
563+
)
564+
565+
// Verify the full context was returned (middleware executed)
566+
await expect(
567+
page.getByTestId('fn-comparison-reexportedFactoryFn'),
568+
).toContainText('equal')
569+
})
570+
571+
test('star re-exported server function factory middleware executes correctly', async ({
572+
page,
573+
}) => {
574+
// This test specifically verifies that when a server function factory is re-exported
575+
// using `export * from './module'` syntax, the middleware still executes.
576+
// Previously, this syntax caused middleware to be silently skipped.
577+
await page.goto('/factory')
578+
579+
await expect(page.getByTestId('factory-route-component')).toBeInViewport()
580+
581+
// Click the button for the star re-exported factory function
582+
await page.getByTestId('btn-fn-starReexportedFactoryFn').click()
583+
584+
// Wait for the result
585+
await expect(
586+
page.getByTestId('fn-result-starReexportedFactoryFn'),
587+
).toContainText('star-reexport-middleware-executed')
588+
589+
// Verify the full context was returned (middleware executed)
590+
await expect(
591+
page.getByTestId('fn-comparison-starReexportedFactoryFn'),
592+
).toContainText('equal')
593+
})

packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ interface ModuleInfo {
5151
ast: ReturnType<typeof parseAst>
5252
bindings: Map<string, Binding>
5353
exports: Map<string, ExportEntry>
54+
// Track `export * from './module'` declarations for re-export resolution
55+
reExportAllSources: Array<string>
5456
}
5557

5658
export class ServerFnCompiler {
@@ -86,6 +88,7 @@ export class ServerFnCompiler {
8688
exports: new Map(),
8789
code: '',
8890
id: libId,
91+
reExportAllSources: [],
8992
}
9093
this.moduleCache.set(libId, rootModule)
9194
}
@@ -116,6 +119,7 @@ export class ServerFnCompiler {
116119

117120
const bindings = new Map<string, Binding>()
118121
const exports = new Map<string, ExportEntry>()
122+
const reExportAllSources: Array<string> = []
119123

120124
// we are only interested in top-level bindings, hence we don't traverse the AST
121125
// instead we only iterate over the program body
@@ -178,6 +182,16 @@ export class ServerFnCompiler {
178182
? sp.exported.name
179183
: sp.exported.value
180184
exports.set(exported, { tag: 'Normal', name: local })
185+
186+
// When re-exporting from another module (export { foo } from './module'),
187+
// create an import binding so the server function can be resolved
188+
if (node.source) {
189+
bindings.set(local, {
190+
type: 'import',
191+
source: node.source.value,
192+
importedName: local,
193+
})
194+
}
181195
}
182196
}
183197
} else if (t.isExportDefaultDeclaration(node)) {
@@ -189,10 +203,21 @@ export class ServerFnCompiler {
189203
bindings.set(synth, { type: 'var', init: d as t.Expression })
190204
exports.set('default', { tag: 'Default', name: synth })
191205
}
206+
} else if (t.isExportAllDeclaration(node)) {
207+
// Handle `export * from './module'` syntax
208+
// Track the source so we can look up exports from it when needed
209+
reExportAllSources.push(node.source.value)
192210
}
193211
}
194212

195-
const info: ModuleInfo = { code, id, ast, bindings, exports }
213+
const info: ModuleInfo = {
214+
code,
215+
id,
216+
ast,
217+
bindings,
218+
exports,
219+
reExportAllSources,
220+
}
196221
this.moduleCache.set(id, info)
197222
return info
198223
}
@@ -343,7 +368,43 @@ export class ServerFnCompiler {
343368

344369
const importedModule = await this.getModuleInfo(target)
345370

371+
// Try to find the export in the module's direct exports
346372
const moduleExport = importedModule.exports.get(binding.importedName)
373+
374+
// If not found directly, check re-export-all sources (`export * from './module'`)
375+
if (!moduleExport && importedModule.reExportAllSources.length > 0) {
376+
for (const reExportSource of importedModule.reExportAllSources) {
377+
const reExportTarget = await this.options.resolveId(
378+
reExportSource,
379+
importedModule.id,
380+
)
381+
if (reExportTarget) {
382+
const reExportModule = await this.getModuleInfo(reExportTarget)
383+
const reExportEntry = reExportModule.exports.get(
384+
binding.importedName,
385+
)
386+
if (reExportEntry) {
387+
// Found the export in a re-exported module, resolve from there
388+
const reExportBinding = reExportModule.bindings.get(
389+
reExportEntry.name,
390+
)
391+
if (reExportBinding) {
392+
if (reExportBinding.resolvedKind) {
393+
return reExportBinding.resolvedKind
394+
}
395+
const resolvedKind = await this.resolveBindingKind(
396+
reExportBinding,
397+
reExportModule.id,
398+
visited,
399+
)
400+
reExportBinding.resolvedKind = resolvedKind
401+
return resolvedKind
402+
}
403+
}
404+
}
405+
}
406+
}
407+
347408
if (!moduleExport) {
348409
return 'None'
349410
}

0 commit comments

Comments
 (0)