Skip to content

Commit d9b07e8

Browse files
shkumbinhasaniclaudeSheraff
authored
perf(start-plugin-core): skip resolving member calls on non-namespace receivers (#8758)
* perf(start-plugin-core): skip resolving member calls on non-namespace receivers A top-level `a.b()` initializer was a direct-call candidate for any `a`, so `const Input = z.object({...})` resolved `z` and, in dev, loaded and parsed all of zod through `?server-fn-module-lookup` while every compiled file waited. For a receiver that is not a namespace import, resolution gives the call the kind of the receiver itself, which only yields a candidate when the receiver is an env-only factory and the code calls one of its properties. Skip those calls; namespace calls, identifier calls and method chains resolve as before. Refs #8757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(start-plugin-core): cover member calls across compiler modes --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sheraff <me@florianpellet.com>
1 parent a8f4dce commit d9b07e8

3 files changed

Lines changed: 165 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/start-plugin-core': patch
3+
---
4+
5+
Stop resolving top-level calls such as `z.object()` whose receiver is not a namespace import. They can never be a `createServerOnlyFn` or `createClientOnlyFn` call, and resolving them made the dev compiler load and parse the receiver's module (all of zod, for example).

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,31 @@ function isTopLevelDirectCallCandidate(
425425
)
426426
}
427427

428+
/**
429+
* Whether the call is `receiver.method()` with a receiver that is not a
430+
* namespace import, e.g. `z.object()` with `import { z } from 'zod'`.
431+
*
432+
* Resolution gives such a call the kind of the receiver itself, which only
433+
* yields a direct-call candidate when the receiver is a factory and the code
434+
* calls one of its properties (`createServerOnlyFn.bind()`). Skipping these
435+
* calls saves loading the receiver's module (all of zod, here) only to learn
436+
* that `z` is not a Start factory.
437+
*/
438+
function isMemberCallOnNonNamespaceReceiver(
439+
node: t.CallExpression,
440+
bindings: Map<string, Binding>,
441+
): boolean {
442+
if (
443+
!t.isMemberExpression(node.callee) ||
444+
!t.isIdentifier(node.callee.object)
445+
) {
446+
return false
447+
}
448+
449+
const binding = bindings.get(node.callee.object.name)
450+
return !(binding?.type === 'import' && binding.importedName === '*')
451+
}
452+
428453
function isDirectCallCandidateForKind(
429454
kind: Exclude<LookupKind, 'ClientOnlyJSX'>,
430455
externalLookupSetup?: Map<ExternalLookupKind, DirectCallSetup>,
@@ -1125,7 +1150,13 @@ export class StartCompiler {
11251150
}
11261151
}
11271152

1128-
if (isTopLevelDirectCallCandidate(path)) {
1153+
if (
1154+
isTopLevelDirectCallCandidate(path) &&
1155+
!isMemberCallOnNonNamespaceReceiver(
1156+
node,
1157+
moduleInfo.bindings,
1158+
)
1159+
) {
11291160
candidatePaths.push({ path })
11301161
}
11311162
},
@@ -1172,7 +1203,11 @@ export class StartCompiler {
11721203
}
11731204
}
11741205

1175-
if (checkDirectCalls && isTopLevelDirectCallCandidate(path)) {
1206+
if (
1207+
checkDirectCalls &&
1208+
isTopLevelDirectCallCandidate(path) &&
1209+
!isMemberCallOnNonNamespaceReceiver(node, moduleInfo.bindings)
1210+
) {
11761211
candidatePaths.push({ path })
11771212
return
11781213
}

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,129 @@ export const factory = createServerFn().middleware([middleware]);`
14071407
).resolves.toBeNull()
14081408
})
14091409

1410+
describe.each(['build', 'dev'] as const)(
1411+
'top-level member calls in %s mode',
1412+
(mode) => {
1413+
function createCompilerWithModules(
1414+
env: 'client' | 'server',
1415+
virtualModules: Record<string, string>,
1416+
) {
1417+
const resolved: Array<string> = []
1418+
const loaded: Array<string> = []
1419+
const compiler: StartCompiler = new StartCompiler({
1420+
...getDefaultTestOptions(env),
1421+
env,
1422+
mode,
1423+
devServerFnModuleSpecifierEncoder: ({ extractedFilename, root }) =>
1424+
`/@id${extractedFilename.slice(root.length)}`,
1425+
lookupKinds: getLookupKindsForEnv(env),
1426+
lookupConfigurations: getLookupConfigurationsForEnv(env, 'react'),
1427+
getKnownServerFns: () => ({}),
1428+
resolveId: async (id) => {
1429+
resolved.push(id)
1430+
return id in virtualModules ? id : null
1431+
},
1432+
loadModule: async (id) => {
1433+
loaded.push(id)
1434+
const code = virtualModules[id]
1435+
if (code) {
1436+
compiler.ingestModule({ code, id })
1437+
}
1438+
},
1439+
})
1440+
return { compiler, resolved, loaded }
1441+
}
1442+
1443+
describe.each(['fast', 'full'] as const)('%s traversal', (traversal) => {
1444+
// An isomorphic function requires full traversal; server functions alone
1445+
// use the compiler's fast path. Let normal code detection choose the path.
1446+
const traversalCode =
1447+
traversal === 'full'
1448+
? `import { createIsomorphicFn } from '@tanstack/react-start'
1449+
export const iso = createIsomorphicFn().client(() => 'client').server(() => 'server')`
1450+
: ''
1451+
1452+
test.each(['client', 'ssr', 'provider'] as const)(
1453+
'does not resolve or load named-import receivers in %s output',
1454+
async (output) => {
1455+
const env = output === 'client' ? 'client' : 'server'
1456+
const { compiler, resolved, loaded } = createCompilerWithModules(
1457+
env,
1458+
{
1459+
zod: `export const z = { object: (shape) => shape, string: () => ({}) }`,
1460+
'./api': `export const api = { users: () => [] }`,
1461+
},
1462+
)
1463+
const code = `
1464+
${traversalCode}
1465+
import { createServerFn } from '@tanstack/react-start'
1466+
import { z } from 'zod'
1467+
import { api } from './api'
1468+
1469+
const Input = z.object({ id: z.string() })
1470+
const users = api.users()
1471+
1472+
export const getUser = createServerFn()
1473+
.validator(Input)
1474+
.handler(async ({ data }) => ({ id: data.id, users }))
1475+
`
1476+
1477+
const result = await compiler.compile({
1478+
id:
1479+
'/test/get-user.ts' +
1480+
(output === 'provider' ? '?tss-serverfn-split' : ''),
1481+
code,
1482+
detectedKinds: detectKindsInCode(code, env),
1483+
})
1484+
1485+
expect(result).not.toBeNull()
1486+
const rpc = {
1487+
client: 'createClientRpc',
1488+
ssr: 'createSsrRpc',
1489+
provider: 'createServerRpc',
1490+
}
1491+
expect(result!.code).toContain(rpc[output])
1492+
expect(resolved).toEqual([])
1493+
expect(loaded).toEqual([])
1494+
},
1495+
)
1496+
1497+
test.each(['client', 'server'] as const)(
1498+
'still compiles a renamed env-only factory called through a namespace import on %s',
1499+
async (env) => {
1500+
const { compiler } = createCompilerWithModules(env, {
1501+
'./factory': `export { createServerOnlyFn as serverOnly } from '@tanstack/start-fn-stubs'`,
1502+
})
1503+
const code = `
1504+
${traversalCode}
1505+
import { createServerFn } from '@tanstack/react-start'
1506+
import * as factory from './factory'
1507+
1508+
export const impl = factory.serverOnly(() => 'server-only-value')
1509+
1510+
export const fn = createServerFn().handler(async () => impl())
1511+
`
1512+
1513+
const result = await compiler.compile({
1514+
id: '/test/namespace-factory.ts',
1515+
code,
1516+
detectedKinds: detectKindsInCode(code, env),
1517+
})
1518+
1519+
expect(result).not.toBeNull()
1520+
if (env === 'server') {
1521+
expect(result!.code).toContain('server-only-value')
1522+
expect(result!.code).not.toContain('factory.serverOnly')
1523+
} else {
1524+
expect(result!.code).not.toContain('server-only-value')
1525+
expect(result!.code).toContain('throw new Error')
1526+
}
1527+
},
1528+
)
1529+
})
1530+
},
1531+
)
1532+
14101533
test.each([
14111534
`(createServerOnlyFn)(() => 'server-value')`,
14121535
`(createIsomorphicFn)().server(() => 'server-value').client(() => 'client-value')`,

0 commit comments

Comments
 (0)