@tanstack/start-plugin-core 1.142.11 → 1.142.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/create-server-fn-plugin/compiler.d.ts +25 -4
- package/dist/esm/create-server-fn-plugin/compiler.js +242 -79
- package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/plugin.js +24 -24
- package/dist/esm/create-server-fn-plugin/plugin.js.map +1 -1
- package/package.json +7 -7
- package/src/create-server-fn-plugin/compiler.ts +397 -111
- package/src/create-server-fn-plugin/plugin.ts +37 -32
|
@@ -68,6 +68,57 @@ const LookupSetup: Record<LookupKind, MethodChainSetup | DirectCallSetup> = {
|
|
|
68
68
|
ClientOnlyFn: { type: 'directCall' },
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Single source of truth for detecting which kinds are present in code
|
|
72
|
+
// These patterns are used for:
|
|
73
|
+
// 1. Pre-scanning code to determine which kinds to look for (before AST parsing)
|
|
74
|
+
// 2. Deriving the plugin's transform code filter
|
|
75
|
+
export const KindDetectionPatterns: Record<LookupKind, RegExp> = {
|
|
76
|
+
ServerFn: /\.handler\s*\(/,
|
|
77
|
+
Middleware: /createMiddleware/,
|
|
78
|
+
IsomorphicFn: /createIsomorphicFn/,
|
|
79
|
+
ServerOnlyFn: /createServerOnlyFn/,
|
|
80
|
+
ClientOnlyFn: /createClientOnlyFn/,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Which kinds are valid for each environment
|
|
84
|
+
export const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>> = {
|
|
85
|
+
client: new Set([
|
|
86
|
+
'Middleware',
|
|
87
|
+
'ServerFn',
|
|
88
|
+
'IsomorphicFn',
|
|
89
|
+
'ServerOnlyFn',
|
|
90
|
+
'ClientOnlyFn',
|
|
91
|
+
] as const),
|
|
92
|
+
server: new Set([
|
|
93
|
+
'ServerFn',
|
|
94
|
+
'IsomorphicFn',
|
|
95
|
+
'ServerOnlyFn',
|
|
96
|
+
'ClientOnlyFn',
|
|
97
|
+
] as const),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detects which LookupKinds are present in the code using string matching.
|
|
102
|
+
* This is a fast pre-scan before AST parsing to limit the work done during compilation.
|
|
103
|
+
*/
|
|
104
|
+
export function detectKindsInCode(
|
|
105
|
+
code: string,
|
|
106
|
+
env: 'client' | 'server',
|
|
107
|
+
): Set<LookupKind> {
|
|
108
|
+
const detected = new Set<LookupKind>()
|
|
109
|
+
const validForEnv = LookupKindsPerEnv[env]
|
|
110
|
+
|
|
111
|
+
for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array<
|
|
112
|
+
[LookupKind, RegExp]
|
|
113
|
+
>) {
|
|
114
|
+
if (validForEnv.has(kind) && pattern.test(code)) {
|
|
115
|
+
detected.add(kind)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return detected
|
|
120
|
+
}
|
|
121
|
+
|
|
71
122
|
// Pre-computed map: identifier name -> Set<LookupKind> for fast candidate detection (method chain only)
|
|
72
123
|
// Multiple kinds can share the same identifier (e.g., 'server' and 'client' are used by both Middleware and IsomorphicFn)
|
|
73
124
|
const IdentifierToKinds = new Map<string, Set<LookupKind>>()
|
|
@@ -86,6 +137,16 @@ for (const [kind, setup] of Object.entries(LookupSetup) as Array<
|
|
|
86
137
|
}
|
|
87
138
|
}
|
|
88
139
|
|
|
140
|
+
// Known factory function names for direct call and root-as-candidate patterns
|
|
141
|
+
// These are the names that, when called directly, create a new function.
|
|
142
|
+
// Used to filter nested candidates - we only want to include actual factory calls,
|
|
143
|
+
// not invocations of already-created functions (e.g., `myServerFn()` should NOT be a candidate)
|
|
144
|
+
const DirectCallFactoryNames = new Set([
|
|
145
|
+
'createServerOnlyFn',
|
|
146
|
+
'createClientOnlyFn',
|
|
147
|
+
'createIsomorphicFn',
|
|
148
|
+
])
|
|
149
|
+
|
|
89
150
|
export type LookupConfig = {
|
|
90
151
|
libName: string
|
|
91
152
|
rootExport: string
|
|
@@ -100,13 +161,94 @@ interface ModuleInfo {
|
|
|
100
161
|
reExportAllSources: Array<string>
|
|
101
162
|
}
|
|
102
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Computes whether any file kinds need direct-call candidate detection.
|
|
166
|
+
* This includes both directCall types (ServerOnlyFn, ClientOnlyFn) and
|
|
167
|
+
* allowRootAsCandidate types (IsomorphicFn).
|
|
168
|
+
*/
|
|
169
|
+
function needsDirectCallDetection(kinds: Set<LookupKind>): boolean {
|
|
170
|
+
for (const kind of kinds) {
|
|
171
|
+
const setup = LookupSetup[kind]
|
|
172
|
+
if (setup.type === 'directCall' || setup.allowRootAsCandidate) {
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Checks if all kinds in the set are guaranteed to be top-level only.
|
|
181
|
+
* Only ServerFn is always declared at module level (must be assigned to a variable).
|
|
182
|
+
* Middleware, IsomorphicFn, ServerOnlyFn, ClientOnlyFn can be nested inside functions.
|
|
183
|
+
* When all kinds are top-level-only, we can use a fast scan instead of full traversal.
|
|
184
|
+
*/
|
|
185
|
+
function areAllKindsTopLevelOnly(kinds: Set<LookupKind>): boolean {
|
|
186
|
+
return kinds.size === 1 && kinds.has('ServerFn')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a CallExpression is a direct-call candidate for NESTED detection.
|
|
191
|
+
* Returns true if the callee is a known factory function name.
|
|
192
|
+
* This is stricter than top-level detection because we need to filter out
|
|
193
|
+
* invocations of existing server functions (e.g., `myServerFn()`).
|
|
194
|
+
*/
|
|
195
|
+
function isNestedDirectCallCandidate(node: t.CallExpression): boolean {
|
|
196
|
+
let calleeName: string | undefined
|
|
197
|
+
if (t.isIdentifier(node.callee)) {
|
|
198
|
+
calleeName = node.callee.name
|
|
199
|
+
} else if (
|
|
200
|
+
t.isMemberExpression(node.callee) &&
|
|
201
|
+
t.isIdentifier(node.callee.property)
|
|
202
|
+
) {
|
|
203
|
+
calleeName = node.callee.property.name
|
|
204
|
+
}
|
|
205
|
+
return calleeName !== undefined && DirectCallFactoryNames.has(calleeName)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Checks if a CallExpression path is a top-level direct-call candidate.
|
|
210
|
+
* Top-level means the call is the init of a VariableDeclarator at program level.
|
|
211
|
+
* We accept any simple identifier call or namespace call at top level
|
|
212
|
+
* (e.g., `isomorphicFn()`, `TanStackStart.createServerOnlyFn()`) and let
|
|
213
|
+
* resolution verify it. This handles renamed imports.
|
|
214
|
+
*/
|
|
215
|
+
function isTopLevelDirectCallCandidate(
|
|
216
|
+
path: babel.NodePath<t.CallExpression>,
|
|
217
|
+
): boolean {
|
|
218
|
+
const node = path.node
|
|
219
|
+
|
|
220
|
+
// Must be a simple identifier call or namespace call
|
|
221
|
+
const isSimpleCall =
|
|
222
|
+
t.isIdentifier(node.callee) ||
|
|
223
|
+
(t.isMemberExpression(node.callee) &&
|
|
224
|
+
t.isIdentifier(node.callee.object) &&
|
|
225
|
+
t.isIdentifier(node.callee.property))
|
|
226
|
+
|
|
227
|
+
if (!isSimpleCall) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Must be top-level: VariableDeclarator -> VariableDeclaration -> Program
|
|
232
|
+
const parent = path.parent
|
|
233
|
+
if (!t.isVariableDeclarator(parent) || parent.init !== node) {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
const grandParent = path.parentPath.parent
|
|
237
|
+
if (!t.isVariableDeclaration(grandParent)) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
return t.isProgram(path.parentPath.parentPath?.parent)
|
|
241
|
+
}
|
|
242
|
+
|
|
103
243
|
export class ServerFnCompiler {
|
|
104
244
|
private moduleCache = new Map<string, ModuleInfo>()
|
|
105
245
|
private initialized = false
|
|
106
246
|
private validLookupKinds: Set<LookupKind>
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
247
|
+
private resolveIdCache = new Map<string, string | null>()
|
|
248
|
+
private exportResolutionCache = new Map<
|
|
249
|
+
string,
|
|
250
|
+
Map<string, { moduleInfo: ModuleInfo; binding: Binding } | null>
|
|
251
|
+
>()
|
|
110
252
|
// Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
|
|
111
253
|
// Maps: libName → (exportName → Kind)
|
|
112
254
|
// This allows O(1) resolution for the common case without async resolveId calls
|
|
@@ -119,21 +261,42 @@ export class ServerFnCompiler {
|
|
|
119
261
|
lookupKinds: Set<LookupKind>
|
|
120
262
|
loadModule: (id: string) => Promise<void>
|
|
121
263
|
resolveId: (id: string, importer?: string) => Promise<string | null>
|
|
264
|
+
/**
|
|
265
|
+
* In 'build' mode, resolution results are cached for performance.
|
|
266
|
+
* In 'dev' mode (default), caching is disabled to avoid invalidation complexity with HMR.
|
|
267
|
+
*/
|
|
268
|
+
mode?: 'dev' | 'build'
|
|
122
269
|
},
|
|
123
270
|
) {
|
|
124
271
|
this.validLookupKinds = options.lookupKinds
|
|
272
|
+
}
|
|
125
273
|
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} else if (setup.allowRootAsCandidate) {
|
|
134
|
-
this.hasRootAsCandidateKinds = true
|
|
135
|
-
}
|
|
274
|
+
private get mode(): 'dev' | 'build' {
|
|
275
|
+
return this.options.mode ?? 'dev'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async resolveIdCached(id: string, importer?: string) {
|
|
279
|
+
if (this.mode === 'dev') {
|
|
280
|
+
return this.options.resolveId(id, importer)
|
|
136
281
|
}
|
|
282
|
+
|
|
283
|
+
const cacheKey = importer ? `${importer}::${id}` : id
|
|
284
|
+
const cached = this.resolveIdCache.get(cacheKey)
|
|
285
|
+
if (cached !== undefined) {
|
|
286
|
+
return cached
|
|
287
|
+
}
|
|
288
|
+
const resolved = await this.options.resolveId(id, importer)
|
|
289
|
+
this.resolveIdCache.set(cacheKey, resolved)
|
|
290
|
+
return resolved
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private getExportResolutionCache(moduleId: string) {
|
|
294
|
+
let cache = this.exportResolutionCache.get(moduleId)
|
|
295
|
+
if (!cache) {
|
|
296
|
+
cache = new Map()
|
|
297
|
+
this.exportResolutionCache.set(moduleId, cache)
|
|
298
|
+
}
|
|
299
|
+
return cache
|
|
137
300
|
}
|
|
138
301
|
|
|
139
302
|
private async init() {
|
|
@@ -159,7 +322,7 @@ export class ServerFnCompiler {
|
|
|
159
322
|
}
|
|
160
323
|
libExports.set(config.rootExport, config.kind)
|
|
161
324
|
|
|
162
|
-
const libId = await this.
|
|
325
|
+
const libId = await this.resolveIdCached(config.libName)
|
|
163
326
|
if (!libId) {
|
|
164
327
|
throw new Error(`could not resolve "${config.libName}"`)
|
|
165
328
|
}
|
|
@@ -196,9 +359,14 @@ export class ServerFnCompiler {
|
|
|
196
359
|
this.initialized = true
|
|
197
360
|
}
|
|
198
361
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Extracts bindings and exports from an already-parsed AST.
|
|
364
|
+
* This is the core logic shared by ingestModule and ingestModuleFromAst.
|
|
365
|
+
*/
|
|
366
|
+
private extractModuleInfo(
|
|
367
|
+
ast: ReturnType<typeof parseAst>,
|
|
368
|
+
id: string,
|
|
369
|
+
): ModuleInfo {
|
|
202
370
|
const bindings = new Map<string, Binding>()
|
|
203
371
|
const exports = new Map<string, ExportEntry>()
|
|
204
372
|
const reExportAllSources: Array<string> = []
|
|
@@ -299,10 +467,19 @@ export class ServerFnCompiler {
|
|
|
299
467
|
reExportAllSources,
|
|
300
468
|
}
|
|
301
469
|
this.moduleCache.set(id, info)
|
|
470
|
+
return info
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
public ingestModule({ code, id }: { code: string; id: string }) {
|
|
474
|
+
const ast = parseAst({ code })
|
|
475
|
+
const info = this.extractModuleInfo(ast, id)
|
|
302
476
|
return { info, ast }
|
|
303
477
|
}
|
|
304
478
|
|
|
305
479
|
public invalidateModule(id: string) {
|
|
480
|
+
// Note: Resolution caches (resolveIdCache, exportResolutionCache) are only
|
|
481
|
+
// used in build mode where there's no HMR. In dev mode, caching is disabled,
|
|
482
|
+
// so we only need to invalidate the moduleCache here.
|
|
306
483
|
return this.moduleCache.delete(id)
|
|
307
484
|
}
|
|
308
485
|
|
|
@@ -310,68 +487,184 @@ export class ServerFnCompiler {
|
|
|
310
487
|
code,
|
|
311
488
|
id,
|
|
312
489
|
isProviderFile,
|
|
490
|
+
detectedKinds,
|
|
313
491
|
}: {
|
|
314
492
|
code: string
|
|
315
493
|
id: string
|
|
316
494
|
isProviderFile: boolean
|
|
495
|
+
/** Pre-detected kinds present in this file. If not provided, all valid kinds are checked. */
|
|
496
|
+
detectedKinds?: Set<LookupKind>
|
|
317
497
|
}) {
|
|
318
498
|
if (!this.initialized) {
|
|
319
499
|
await this.init()
|
|
320
500
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
501
|
+
|
|
502
|
+
// Use detected kinds if provided, otherwise fall back to all valid kinds for this env
|
|
503
|
+
const fileKinds = detectedKinds
|
|
504
|
+
? new Set([...detectedKinds].filter((k) => this.validLookupKinds.has(k)))
|
|
505
|
+
: this.validLookupKinds
|
|
506
|
+
|
|
507
|
+
// Early exit if no kinds to process
|
|
508
|
+
if (fileKinds.size === 0) {
|
|
326
509
|
return null
|
|
327
510
|
}
|
|
328
511
|
|
|
329
|
-
|
|
330
|
-
//
|
|
512
|
+
const checkDirectCalls = needsDirectCallDetection(fileKinds)
|
|
513
|
+
// Optimization: ServerFn is always a top-level declaration (must be assigned to a variable).
|
|
514
|
+
// If the file only has ServerFn, we can skip full AST traversal and only visit
|
|
515
|
+
// the specific top-level declarations that have candidates.
|
|
516
|
+
const canUseFastPath = areAllKindsTopLevelOnly(fileKinds)
|
|
517
|
+
|
|
518
|
+
// Always parse and extract module info upfront.
|
|
519
|
+
// This ensures the module is cached for import resolution even if no candidates are found.
|
|
520
|
+
const { ast } = this.ingestModule({ code, id })
|
|
521
|
+
|
|
522
|
+
// Single-pass traversal to:
|
|
523
|
+
// 1. Collect candidate paths (only candidates, not all CallExpressions)
|
|
524
|
+
// 2. Build a map for looking up paths of nested calls in method chains
|
|
525
|
+
const candidatePaths: Array<babel.NodePath<t.CallExpression>> = []
|
|
526
|
+
// Map for nested chain lookup - only populated for CallExpressions that are
|
|
527
|
+
// part of a method chain (callee.object is a CallExpression)
|
|
528
|
+
const chainCallPaths = new Map<
|
|
529
|
+
t.CallExpression,
|
|
530
|
+
babel.NodePath<t.CallExpression>
|
|
531
|
+
>()
|
|
532
|
+
|
|
533
|
+
if (canUseFastPath) {
|
|
534
|
+
// Fast path: only visit top-level statements that have potential candidates
|
|
535
|
+
|
|
536
|
+
// Collect indices of top-level statements that contain candidates
|
|
537
|
+
const candidateIndices: Array<number> = []
|
|
538
|
+
for (let i = 0; i < ast.program.body.length; i++) {
|
|
539
|
+
const node = ast.program.body[i]!
|
|
540
|
+
let declarations: Array<t.VariableDeclarator> | undefined
|
|
541
|
+
|
|
542
|
+
if (t.isVariableDeclaration(node)) {
|
|
543
|
+
declarations = node.declarations
|
|
544
|
+
} else if (t.isExportNamedDeclaration(node) && node.declaration) {
|
|
545
|
+
if (t.isVariableDeclaration(node.declaration)) {
|
|
546
|
+
declarations = node.declaration.declarations
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (declarations) {
|
|
551
|
+
for (const decl of declarations) {
|
|
552
|
+
if (decl.init && t.isCallExpression(decl.init)) {
|
|
553
|
+
if (isMethodChainCandidate(decl.init, fileKinds)) {
|
|
554
|
+
candidateIndices.push(i)
|
|
555
|
+
break // Only need to mark this statement once
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Early exit: no potential candidates found at top level
|
|
563
|
+
if (candidateIndices.length === 0) {
|
|
564
|
+
return null
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Targeted traversal: only visit the specific statements that have candidates
|
|
568
|
+
// This is much faster than traversing the entire AST
|
|
569
|
+
babel.traverse(ast, {
|
|
570
|
+
Program(programPath) {
|
|
571
|
+
const bodyPaths = programPath.get('body')
|
|
572
|
+
for (const idx of candidateIndices) {
|
|
573
|
+
const stmtPath = bodyPaths[idx]
|
|
574
|
+
if (!stmtPath) continue
|
|
575
|
+
|
|
576
|
+
// Traverse only this statement's subtree
|
|
577
|
+
stmtPath.traverse({
|
|
578
|
+
CallExpression(path) {
|
|
579
|
+
const node = path.node
|
|
580
|
+
const parent = path.parent
|
|
581
|
+
|
|
582
|
+
// Check if this call is part of a larger chain (inner call)
|
|
583
|
+
if (
|
|
584
|
+
t.isMemberExpression(parent) &&
|
|
585
|
+
t.isCallExpression(path.parentPath.parent)
|
|
586
|
+
) {
|
|
587
|
+
chainCallPaths.set(node, path)
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Method chain pattern
|
|
592
|
+
if (isMethodChainCandidate(node, fileKinds)) {
|
|
593
|
+
candidatePaths.push(path)
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
// Stop traversal after processing Program
|
|
599
|
+
programPath.stop()
|
|
600
|
+
},
|
|
601
|
+
})
|
|
602
|
+
} else {
|
|
603
|
+
// Normal path: full traversal for non-fast-path kinds
|
|
604
|
+
babel.traverse(ast, {
|
|
605
|
+
CallExpression: (path) => {
|
|
606
|
+
const node = path.node
|
|
607
|
+
const parent = path.parent
|
|
608
|
+
|
|
609
|
+
// Check if this call is part of a larger chain (inner call)
|
|
610
|
+
// If so, store it for method chain lookup but don't treat as candidate
|
|
611
|
+
if (
|
|
612
|
+
t.isMemberExpression(parent) &&
|
|
613
|
+
t.isCallExpression(path.parentPath.parent)
|
|
614
|
+
) {
|
|
615
|
+
// This is an inner call in a chain - store for later lookup
|
|
616
|
+
chainCallPaths.set(node, path)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.)
|
|
621
|
+
if (isMethodChainCandidate(node, fileKinds)) {
|
|
622
|
+
candidatePaths.push(path)
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Pattern 2: Direct call pattern
|
|
627
|
+
if (checkDirectCalls) {
|
|
628
|
+
if (isTopLevelDirectCallCandidate(path)) {
|
|
629
|
+
candidatePaths.push(path)
|
|
630
|
+
} else if (isNestedDirectCallCandidate(node)) {
|
|
631
|
+
candidatePaths.push(path)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (candidatePaths.length === 0) {
|
|
639
|
+
return null
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Resolve all candidates in parallel to determine their kinds
|
|
331
643
|
const resolvedCandidates = await Promise.all(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
kind: await this.resolveExprKind(
|
|
644
|
+
candidatePaths.map(async (path) => ({
|
|
645
|
+
path,
|
|
646
|
+
kind: await this.resolveExprKind(path.node, id),
|
|
335
647
|
})),
|
|
336
648
|
)
|
|
337
649
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (toRewriteMap.size === 0) {
|
|
650
|
+
// Filter to valid candidates
|
|
651
|
+
const validCandidates = resolvedCandidates.filter(({ kind }) =>
|
|
652
|
+
this.validLookupKinds.has(kind as LookupKind),
|
|
653
|
+
) as Array<{ path: babel.NodePath<t.CallExpression>; kind: LookupKind }>
|
|
654
|
+
|
|
655
|
+
if (validCandidates.length === 0) {
|
|
347
656
|
return null
|
|
348
657
|
}
|
|
349
658
|
|
|
350
|
-
//
|
|
659
|
+
// Process valid candidates to collect method chains
|
|
351
660
|
const pathsToRewrite: Array<{
|
|
352
661
|
path: babel.NodePath<t.CallExpression>
|
|
353
662
|
kind: LookupKind
|
|
354
663
|
methodChain: MethodChainPaths
|
|
355
664
|
}> = []
|
|
356
665
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
t.CallExpression,
|
|
360
|
-
babel.NodePath<t.CallExpression>
|
|
361
|
-
>()
|
|
362
|
-
|
|
363
|
-
babel.traverse(ast, {
|
|
364
|
-
CallExpression(path) {
|
|
365
|
-
callExprPaths.set(path.node, path)
|
|
366
|
-
},
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
// Now process candidates - we can look up any CallExpression path in O(1)
|
|
370
|
-
for (const [node, kind] of toRewriteMap) {
|
|
371
|
-
const path = callExprPaths.get(node)
|
|
372
|
-
if (!path) {
|
|
373
|
-
continue
|
|
374
|
-
}
|
|
666
|
+
for (const { path, kind } of validCandidates) {
|
|
667
|
+
const node = path.node
|
|
375
668
|
|
|
376
669
|
// Collect method chain paths by walking DOWN from root through the chain
|
|
377
670
|
const methodChain: MethodChainPaths = {
|
|
@@ -384,6 +677,8 @@ export class ServerFnCompiler {
|
|
|
384
677
|
|
|
385
678
|
// Walk down the call chain using nodes, look up paths from map
|
|
386
679
|
let currentNode: t.CallExpression = node
|
|
680
|
+
let currentPath: babel.NodePath<t.CallExpression> = path
|
|
681
|
+
|
|
387
682
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
388
683
|
while (true) {
|
|
389
684
|
const callee = currentNode.callee
|
|
@@ -395,7 +690,6 @@ export class ServerFnCompiler {
|
|
|
395
690
|
if (t.isIdentifier(callee.property)) {
|
|
396
691
|
const name = callee.property.name as keyof MethodChainPaths
|
|
397
692
|
if (name in methodChain) {
|
|
398
|
-
const currentPath = callExprPaths.get(currentNode)!
|
|
399
693
|
// Get first argument path
|
|
400
694
|
const args = currentPath.get('arguments')
|
|
401
695
|
const firstArgPath =
|
|
@@ -412,18 +706,17 @@ export class ServerFnCompiler {
|
|
|
412
706
|
break
|
|
413
707
|
}
|
|
414
708
|
currentNode = callee.object
|
|
709
|
+
// Look up path from chain map, or use candidate path if not found
|
|
710
|
+
const nextPath = chainCallPaths.get(currentNode)
|
|
711
|
+
if (!nextPath) {
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
currentPath = nextPath
|
|
415
715
|
}
|
|
416
716
|
|
|
417
717
|
pathsToRewrite.push({ path, kind, methodChain })
|
|
418
718
|
}
|
|
419
719
|
|
|
420
|
-
// Verify we found all candidates (pathsToRewrite should have same size as toRewriteMap had)
|
|
421
|
-
if (pathsToRewrite.length !== toRewriteMap.size) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`Internal error: could not find all paths to rewrite. please file an issue`,
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
720
|
const refIdents = findReferencedIdentifiers(ast)
|
|
428
721
|
|
|
429
722
|
for (const { path, kind, methodChain } of pathsToRewrite) {
|
|
@@ -461,42 +754,6 @@ export class ServerFnCompiler {
|
|
|
461
754
|
})
|
|
462
755
|
}
|
|
463
756
|
|
|
464
|
-
// collects all candidate CallExpressions at top-level
|
|
465
|
-
private collectCandidates(bindings: Map<string, Binding>) {
|
|
466
|
-
const candidates: Array<t.CallExpression> = []
|
|
467
|
-
|
|
468
|
-
for (const binding of bindings.values()) {
|
|
469
|
-
if (binding.type === 'var' && t.isCallExpression(binding.init)) {
|
|
470
|
-
// Pattern 1: Method chain pattern (.handler(), .server(), etc.)
|
|
471
|
-
const methodChainCandidate = isCandidateCallExpression(
|
|
472
|
-
binding.init,
|
|
473
|
-
this.validLookupKinds,
|
|
474
|
-
)
|
|
475
|
-
if (methodChainCandidate) {
|
|
476
|
-
candidates.push(methodChainCandidate)
|
|
477
|
-
continue
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Pattern 2: Direct call pattern
|
|
481
|
-
// Handles:
|
|
482
|
-
// - createServerOnlyFn(), createClientOnlyFn() (direct call kinds)
|
|
483
|
-
// - createIsomorphicFn() (root-as-candidate kinds)
|
|
484
|
-
// - TanStackStart.createServerOnlyFn() (namespace calls)
|
|
485
|
-
if (this.hasDirectCallKinds || this.hasRootAsCandidateKinds) {
|
|
486
|
-
if (
|
|
487
|
-
t.isIdentifier(binding.init.callee) ||
|
|
488
|
-
(t.isMemberExpression(binding.init.callee) &&
|
|
489
|
-
t.isIdentifier(binding.init.callee.property))
|
|
490
|
-
) {
|
|
491
|
-
// Include as candidate - kind resolution will verify it's actually a known export
|
|
492
|
-
candidates.push(binding.init)
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return candidates
|
|
498
|
-
}
|
|
499
|
-
|
|
500
757
|
private async resolveIdentifierKind(
|
|
501
758
|
ident: string,
|
|
502
759
|
id: string,
|
|
@@ -534,6 +791,19 @@ export class ServerFnCompiler {
|
|
|
534
791
|
exportName: string,
|
|
535
792
|
visitedModules = new Set<string>(),
|
|
536
793
|
): Promise<{ moduleInfo: ModuleInfo; binding: Binding } | undefined> {
|
|
794
|
+
const isBuildMode = this.mode === 'build'
|
|
795
|
+
|
|
796
|
+
// Check cache first (only for top-level calls in build mode)
|
|
797
|
+
if (isBuildMode && visitedModules.size === 0) {
|
|
798
|
+
const moduleCache = this.exportResolutionCache.get(moduleInfo.id)
|
|
799
|
+
if (moduleCache) {
|
|
800
|
+
const cached = moduleCache.get(exportName)
|
|
801
|
+
if (cached !== undefined) {
|
|
802
|
+
return cached ?? undefined
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
537
807
|
// Prevent infinite loops in circular re-exports
|
|
538
808
|
if (visitedModules.has(moduleInfo.id)) {
|
|
539
809
|
return undefined
|
|
@@ -545,7 +815,12 @@ export class ServerFnCompiler {
|
|
|
545
815
|
if (directExport) {
|
|
546
816
|
const binding = moduleInfo.bindings.get(directExport.name)
|
|
547
817
|
if (binding) {
|
|
548
|
-
|
|
818
|
+
const result = { moduleInfo, binding }
|
|
819
|
+
// Cache the result (build mode only)
|
|
820
|
+
if (isBuildMode) {
|
|
821
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, result)
|
|
822
|
+
}
|
|
823
|
+
return result
|
|
549
824
|
}
|
|
550
825
|
}
|
|
551
826
|
|
|
@@ -554,10 +829,11 @@ export class ServerFnCompiler {
|
|
|
554
829
|
if (moduleInfo.reExportAllSources.length > 0) {
|
|
555
830
|
const results = await Promise.all(
|
|
556
831
|
moduleInfo.reExportAllSources.map(async (reExportSource) => {
|
|
557
|
-
const reExportTarget = await this.
|
|
832
|
+
const reExportTarget = await this.resolveIdCached(
|
|
558
833
|
reExportSource,
|
|
559
834
|
moduleInfo.id,
|
|
560
835
|
)
|
|
836
|
+
|
|
561
837
|
if (reExportTarget) {
|
|
562
838
|
const reExportModule = await this.getModuleInfo(reExportTarget)
|
|
563
839
|
return this.findExportInModule(
|
|
@@ -572,11 +848,19 @@ export class ServerFnCompiler {
|
|
|
572
848
|
// Return the first valid result
|
|
573
849
|
for (const result of results) {
|
|
574
850
|
if (result) {
|
|
851
|
+
// Cache the result (build mode only)
|
|
852
|
+
if (isBuildMode) {
|
|
853
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, result)
|
|
854
|
+
}
|
|
575
855
|
return result
|
|
576
856
|
}
|
|
577
857
|
}
|
|
578
858
|
}
|
|
579
859
|
|
|
860
|
+
// Cache negative result (build mode only)
|
|
861
|
+
if (isBuildMode) {
|
|
862
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, null)
|
|
863
|
+
}
|
|
580
864
|
return undefined
|
|
581
865
|
}
|
|
582
866
|
|
|
@@ -602,7 +886,7 @@ export class ServerFnCompiler {
|
|
|
602
886
|
}
|
|
603
887
|
|
|
604
888
|
// Slow path: resolve through the module graph
|
|
605
|
-
const target = await this.
|
|
889
|
+
const target = await this.resolveIdCached(binding.source, fileId)
|
|
606
890
|
if (!target) {
|
|
607
891
|
return 'None'
|
|
608
892
|
}
|
|
@@ -746,7 +1030,7 @@ export class ServerFnCompiler {
|
|
|
746
1030
|
binding.importedName === '*'
|
|
747
1031
|
) {
|
|
748
1032
|
// resolve the property from the target module
|
|
749
|
-
const targetModuleId = await this.
|
|
1033
|
+
const targetModuleId = await this.resolveIdCached(
|
|
750
1034
|
binding.source,
|
|
751
1035
|
fileId,
|
|
752
1036
|
)
|
|
@@ -793,15 +1077,17 @@ export class ServerFnCompiler {
|
|
|
793
1077
|
}
|
|
794
1078
|
}
|
|
795
1079
|
|
|
796
|
-
|
|
797
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* Checks if a CallExpression has a method chain pattern that matches any of the lookup kinds.
|
|
1082
|
+
* E.g., `.handler()`, `.server()`, `.client()`, `.createMiddlewares()`
|
|
1083
|
+
*/
|
|
1084
|
+
function isMethodChainCandidate(
|
|
1085
|
+
node: t.CallExpression,
|
|
798
1086
|
lookupKinds: Set<LookupKind>,
|
|
799
|
-
):
|
|
800
|
-
if (!t.isCallExpression(node)) return undefined
|
|
801
|
-
|
|
1087
|
+
): boolean {
|
|
802
1088
|
const callee = node.callee
|
|
803
1089
|
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) {
|
|
804
|
-
return
|
|
1090
|
+
return false
|
|
805
1091
|
}
|
|
806
1092
|
|
|
807
1093
|
// Use pre-computed map for O(1) lookup
|
|
@@ -811,10 +1097,10 @@ function isCandidateCallExpression(
|
|
|
811
1097
|
// Check if any of the possible kinds are in the valid lookup kinds
|
|
812
1098
|
for (const kind of possibleKinds) {
|
|
813
1099
|
if (lookupKinds.has(kind)) {
|
|
814
|
-
return
|
|
1100
|
+
return true
|
|
815
1101
|
}
|
|
816
1102
|
}
|
|
817
1103
|
}
|
|
818
1104
|
|
|
819
|
-
return
|
|
1105
|
+
return false
|
|
820
1106
|
}
|