Skip to content

Commit d3f20fb

Browse files
fix(start-plugin-core): reuse deduped server function ids across compilers (#7153)
1 parent b059766 commit d3f20fb

9 files changed

Lines changed: 119 additions & 4 deletions

File tree

.changeset/flat-radios-smile.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/start-plugin-core': patch
3+
---
4+
5+
Reuse previously discovered server function IDs across compiler instances so custom `generateFunctionId` values stay stable when duplicate IDs are deduplicated during build.
6+
7+
This fixes cases where different build environments could assign different deduped IDs to the same server functions, which could cause requests to resolve to the wrong handler.

packages/start-plugin-core/src/start-compiler/compiler.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ export class StartCompiler {
350350
* Returns the currently known server functions from previous builds.
351351
* Used by server callers to look up canonical extracted filenames.
352352
*/
353-
getKnownServerFns?: () => Record<string, ServerFn>
353+
getKnownServerFns: () => Record<string, ServerFn>
354354
devServerFnModuleSpecifierEncoder?: DevServerFnModuleSpecifierEncoder
355355
},
356356
) {
@@ -394,8 +394,18 @@ export class StartCompiler {
394394
const entryId = `${opts.filename}--${opts.functionName}`
395395
let functionId = this.entryIdToFunctionId.get(entryId)
396396
if (functionId === undefined) {
397+
const knownFn = Object.values(this.options.getKnownServerFns()).find(
398+
(serverFn) =>
399+
serverFn.functionName === opts.functionName &&
400+
serverFn.extractedFilename === opts.extractedFilename,
401+
)
402+
403+
if (knownFn) {
404+
functionId = knownFn.functionId
405+
}
406+
397407
if (this.options.generateFunctionId) {
398-
functionId = this.options.generateFunctionId({
408+
functionId ??= this.options.generateFunctionId({
399409
filename: opts.filename,
400410
functionName: opts.functionName,
401411
})
@@ -920,7 +930,7 @@ export class StartCompiler {
920930
providerEnvName: this.options.providerEnvName,
921931

922932
generateFunctionId: (opts) => this.generateFunctionId(opts),
923-
getKnownServerFns: () => this.options.getKnownServerFns?.() ?? {},
933+
getKnownServerFns: this.options.getKnownServerFns,
924934
onServerFnsById: this.options.onServerFnsById,
925935
}
926936

packages/start-plugin-core/src/start-compiler/host.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface CreateStartCompilerOptions {
1616
mode: 'dev' | 'build'
1717
generateFunctionId?: GenerateFunctionIdFnOptional
1818
onServerFnsById?: (d: Record<string, ServerFn>) => void
19-
getKnownServerFns?: () => Record<string, ServerFn>
19+
getKnownServerFns: () => Record<string, ServerFn>
2020
encodeModuleSpecifierInDev?: DevServerFnModuleSpecifierEncoder
2121
loadModule: (id: string) => Promise<void>
2222
resolveId: (id: string, importer?: string) => Promise<string | null>

packages/start-plugin-core/tests/clientOnlyJSX/clientOnlyJSX.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async function compile(opts: {
3838
kind: 'ClientOnlyJSX',
3939
},
4040
],
41+
getKnownServerFns: () => ({}),
4142
resolveId: async (id) => {
4243
return id
4344
},

packages/start-plugin-core/tests/compiler.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function createFullCompiler(env: 'client' | 'server') {
6262
...getDefaultTestOptions(env),
6363
lookupKinds,
6464
lookupConfigurations,
65+
getKnownServerFns: () => ({}),
6566
loadModule: async () => {},
6667
resolveId: async (id) => id,
6768
mode: 'build',
@@ -438,6 +439,7 @@ test('ingestModule handles empty code gracefully', () => {
438439
...getDefaultTestOptions('client'),
439440
lookupKinds: new Set(['ServerFn']),
440441
lookupConfigurations: [],
442+
getKnownServerFns: () => ({}),
441443
loadModule: async () => {},
442444
resolveId: async (id) => id,
443445
})
@@ -521,6 +523,7 @@ describe('calling result of createServerOnlyFn/createClientOnlyFn', () => {
521523
providerEnvName: 'ssr',
522524
lookupKinds: new Set(['ServerOnlyFn', 'ClientOnlyFn']),
523525
lookupConfigurations: [],
526+
getKnownServerFns: () => ({}),
524527
loadModule: async (id) => {
525528
const code = virtualModules[id]
526529
if (code) {
@@ -613,6 +616,7 @@ describe('re-export chain resolution', () => {
613616
providerEnvName: 'ssr',
614617
lookupKinds,
615618
lookupConfigurations: [],
619+
getKnownServerFns: () => ({}),
616620
loadModule: async (id) => {
617621
const code = virtualModules[id]
618622
if (code) {
@@ -704,6 +708,7 @@ describe('re-export chain resolution', () => {
704708
providerEnvName: 'ssr',
705709
lookupKinds: new Set(['IsomorphicFn', 'ServerOnlyFn', 'ClientOnlyFn']),
706710
lookupConfigurations: [],
711+
getKnownServerFns: () => ({}),
707712
loadModule: async (id) => {
708713
const code = deeperVirtualModules[id]
709714
if (code) {

packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ async function compile(opts: {
3434
kind: 'IsomorphicFn',
3535
},
3636
],
37+
getKnownServerFns: () => ({}),
3738
resolveId: async (id) => {
3839
return id
3940
},

packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async function compile(opts: {
4242
kind: 'Root',
4343
},
4444
],
45+
getKnownServerFns: () => ({}),
4546
resolveId: async (id) => {
4647
return id
4748
},
@@ -93,6 +94,7 @@ describe('createMiddleware compiles correctly', async () => {
9394
kind: 'Root',
9495
},
9596
],
97+
getKnownServerFns: () => ({}),
9698
resolveId: resolveIdMock,
9799
})
98100

@@ -139,6 +141,7 @@ describe('createMiddleware compiles correctly', async () => {
139141
kind: 'Root',
140142
},
141143
],
144+
getKnownServerFns: () => ({}),
142145
resolveId: resolveIdMock,
143146
})
144147

packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async function compile(opts: {
4747
kind: 'Root',
4848
},
4949
],
50+
getKnownServerFns: () => ({}),
5051
resolveId: async (id) => {
5152
return id
5253
},
@@ -250,6 +251,7 @@ describe('createServerFn compiles correctly', async () => {
250251
kind: 'Root',
251252
},
252253
],
254+
getKnownServerFns: () => ({}),
253255
resolveId: resolveIdMock,
254256
mode: 'build',
255257
})
@@ -297,6 +299,7 @@ describe('createServerFn compiles correctly', async () => {
297299
kind: 'Root',
298300
},
299301
],
302+
getKnownServerFns: () => ({}),
300303
resolveId: resolveIdMock,
301304
mode: 'build',
302305
})
@@ -349,6 +352,7 @@ describe('createServerFn compiles correctly', async () => {
349352
kind: 'Root',
350353
},
351354
],
355+
getKnownServerFns: () => ({}),
352356
resolveId: resolveIdMock,
353357
mode: 'build',
354358
})
@@ -407,6 +411,7 @@ describe('createServerFn compiles correctly', async () => {
407411
kind: 'Root',
408412
},
409413
],
414+
getKnownServerFns: () => ({}),
410415
resolveId: resolveIdMock,
411416
mode: 'build',
412417
})
@@ -430,4 +435,86 @@ describe('createServerFn compiles correctly', async () => {
430435
'./factory',
431436
)
432437
})
438+
439+
test('reuses deduped custom IDs across compiler instances', async () => {
440+
const serverFnsById: Record<
441+
string,
442+
{
443+
functionName: string
444+
functionId: string
445+
extractedFilename: string
446+
filename: string
447+
isClientReferenced?: boolean
448+
}
449+
> = {}
450+
451+
function createCompiler() {
452+
return new StartCompiler({
453+
env: 'server',
454+
...getDefaultTestOptions('server'),
455+
mode: 'build',
456+
loadModule: async () => {},
457+
lookupKinds: new Set(['ServerFn']),
458+
lookupConfigurations: [
459+
{
460+
libName: '@tanstack/react-start',
461+
rootExport: 'createServerFn',
462+
kind: 'Root',
463+
},
464+
],
465+
resolveId: async (id) => id,
466+
generateFunctionId: ({ functionName }) =>
467+
functionName === 'greetUser_createServerFn_handler'
468+
? 'constant_id'
469+
: undefined,
470+
getKnownServerFns: () => serverFnsById,
471+
onServerFnsById: (discovered) => {
472+
Object.assign(serverFnsById, discovered)
473+
},
474+
})
475+
}
476+
477+
const firstCompiler = createCompiler()
478+
await firstCompiler.compile({
479+
code: `
480+
import { createServerFn } from '@tanstack/react-start'
481+
export const greetUser = createServerFn().handler(async () => 'first')
482+
`,
483+
id: '/test/src/submit-post-formdata.tsx',
484+
})
485+
486+
await firstCompiler.compile({
487+
code: `
488+
import { createServerFn } from '@tanstack/react-start'
489+
export const greetUser = createServerFn().handler(async () => 'second')
490+
`,
491+
id: '/test/src/formdata-redirect/index.tsx',
492+
})
493+
494+
expect(
495+
Object.values(serverFnsById)
496+
.map((serverFn) => serverFn.functionId)
497+
.sort(),
498+
).toEqual(['constant_id', 'constant_id_1'])
499+
500+
const secondCompiler = createCompiler()
501+
const firstResult = await secondCompiler.compile({
502+
code: `
503+
import { createServerFn } from '@tanstack/react-start'
504+
export const greetUser = createServerFn().handler(async () => 'first')
505+
`,
506+
id: '/test/src/submit-post-formdata.tsx',
507+
})
508+
509+
const secondResult = await secondCompiler.compile({
510+
code: `
511+
import { createServerFn } from '@tanstack/react-start'
512+
export const greetUser = createServerFn().handler(async () => 'second')
513+
`,
514+
id: '/test/src/formdata-redirect/index.tsx',
515+
})
516+
517+
expect(firstResult!.code).toContain('createSsrRpc("constant_id"')
518+
expect(secondResult!.code).toContain('createSsrRpc("constant_id_1"')
519+
})
433520
})

packages/start-plugin-core/tests/envOnly/envOnly.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ async function compile(opts: {
3939
kind: 'ClientOnlyFn',
4040
},
4141
],
42+
getKnownServerFns: () => ({}),
4243
resolveId: async (id) => {
4344
return id
4445
},

0 commit comments

Comments
 (0)