@tanstack/start-plugin-core 1.142.12 → 1.143.0
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 +16 -1
- package/dist/esm/create-server-fn-plugin/compiler.js +200 -29
- package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.d.ts +21 -0
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.js +10 -0
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.js.map +1 -0
- package/dist/esm/create-server-fn-plugin/plugin.js +10 -1
- package/dist/esm/create-server-fn-plugin/plugin.js.map +1 -1
- package/package.json +6 -6
- package/src/create-server-fn-plugin/compiler.ts +311 -38
- package/src/create-server-fn-plugin/handleClientOnlyJSX.ts +32 -0
- package/src/create-server-fn-plugin/plugin.ts +13 -1
|
@@ -10,6 +10,7 @@ import { handleCreateServerFn } from './handleCreateServerFn'
|
|
|
10
10
|
import { handleCreateMiddleware } from './handleCreateMiddleware'
|
|
11
11
|
import { handleCreateIsomorphicFn } from './handleCreateIsomorphicFn'
|
|
12
12
|
import { handleEnvOnlyFn } from './handleEnvOnly'
|
|
13
|
+
import { handleClientOnlyJSX } from './handleClientOnlyJSX'
|
|
13
14
|
import type { MethodChainPaths, RewriteCandidate } from './types'
|
|
14
15
|
|
|
15
16
|
type Binding =
|
|
@@ -38,6 +39,7 @@ export type LookupKind =
|
|
|
38
39
|
| 'IsomorphicFn'
|
|
39
40
|
| 'ServerOnlyFn'
|
|
40
41
|
| 'ClientOnlyFn'
|
|
42
|
+
| 'ClientOnlyJSX'
|
|
41
43
|
|
|
42
44
|
// Detection strategy for each kind
|
|
43
45
|
type MethodChainSetup = {
|
|
@@ -49,8 +51,12 @@ type MethodChainSetup = {
|
|
|
49
51
|
allowRootAsCandidate?: boolean
|
|
50
52
|
}
|
|
51
53
|
type DirectCallSetup = { type: 'directCall' }
|
|
54
|
+
type JSXSetup = { type: 'jsx'; componentName: string }
|
|
52
55
|
|
|
53
|
-
const LookupSetup: Record<
|
|
56
|
+
const LookupSetup: Record<
|
|
57
|
+
LookupKind,
|
|
58
|
+
MethodChainSetup | DirectCallSetup | JSXSetup
|
|
59
|
+
> = {
|
|
54
60
|
ServerFn: {
|
|
55
61
|
type: 'methodChain',
|
|
56
62
|
candidateCallIdentifier: new Set(['handler']),
|
|
@@ -66,6 +72,7 @@ const LookupSetup: Record<LookupKind, MethodChainSetup | DirectCallSetup> = {
|
|
|
66
72
|
},
|
|
67
73
|
ServerOnlyFn: { type: 'directCall' },
|
|
68
74
|
ClientOnlyFn: { type: 'directCall' },
|
|
75
|
+
ClientOnlyJSX: { type: 'jsx', componentName: 'ClientOnly' },
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
// Single source of truth for detecting which kinds are present in code
|
|
@@ -78,6 +85,7 @@ export const KindDetectionPatterns: Record<LookupKind, RegExp> = {
|
|
|
78
85
|
IsomorphicFn: /createIsomorphicFn/,
|
|
79
86
|
ServerOnlyFn: /createServerOnlyFn/,
|
|
80
87
|
ClientOnlyFn: /createClientOnlyFn/,
|
|
88
|
+
ClientOnlyJSX: /<ClientOnly|import\s*\{[^}]*\bClientOnly\b/,
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// Which kinds are valid for each environment
|
|
@@ -94,6 +102,7 @@ export const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>> = {
|
|
|
94
102
|
'IsomorphicFn',
|
|
95
103
|
'ServerOnlyFn',
|
|
96
104
|
'ClientOnlyFn',
|
|
105
|
+
'ClientOnlyJSX', // Only transform on server to remove children
|
|
97
106
|
] as const),
|
|
98
107
|
}
|
|
99
108
|
|
|
@@ -169,13 +178,53 @@ interface ModuleInfo {
|
|
|
169
178
|
function needsDirectCallDetection(kinds: Set<LookupKind>): boolean {
|
|
170
179
|
for (const kind of kinds) {
|
|
171
180
|
const setup = LookupSetup[kind]
|
|
172
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
setup.type === 'directCall' ||
|
|
183
|
+
(setup.type === 'methodChain' && setup.allowRootAsCandidate)
|
|
184
|
+
) {
|
|
173
185
|
return true
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
return false
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Checks if all kinds in the set are guaranteed to be top-level only.
|
|
193
|
+
* Only ServerFn is always declared at module level (must be assigned to a variable).
|
|
194
|
+
* Middleware, IsomorphicFn, ServerOnlyFn, ClientOnlyFn can be nested inside functions.
|
|
195
|
+
* When all kinds are top-level-only, we can use a fast scan instead of full traversal.
|
|
196
|
+
*/
|
|
197
|
+
function areAllKindsTopLevelOnly(kinds: Set<LookupKind>): boolean {
|
|
198
|
+
return kinds.size === 1 && kinds.has('ServerFn')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks if we need to detect JSX elements (e.g., <ClientOnly>).
|
|
203
|
+
*/
|
|
204
|
+
function needsJSXDetection(kinds: Set<LookupKind>): boolean {
|
|
205
|
+
for (const kind of kinds) {
|
|
206
|
+
const setup = LookupSetup[kind]
|
|
207
|
+
if (setup.type === 'jsx') {
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gets the set of JSX component names to detect.
|
|
216
|
+
*/
|
|
217
|
+
function getJSXComponentNames(kinds: Set<LookupKind>): Set<string> {
|
|
218
|
+
const names = new Set<string>()
|
|
219
|
+
for (const kind of kinds) {
|
|
220
|
+
const setup = LookupSetup[kind]
|
|
221
|
+
if (setup.type === 'jsx') {
|
|
222
|
+
names.add(setup.componentName)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return names
|
|
226
|
+
}
|
|
227
|
+
|
|
179
228
|
/**
|
|
180
229
|
* Checks if a CallExpression is a direct-call candidate for NESTED detection.
|
|
181
230
|
* Returns true if the callee is a known factory function name.
|
|
@@ -234,6 +283,11 @@ export class ServerFnCompiler {
|
|
|
234
283
|
private moduleCache = new Map<string, ModuleInfo>()
|
|
235
284
|
private initialized = false
|
|
236
285
|
private validLookupKinds: Set<LookupKind>
|
|
286
|
+
private resolveIdCache = new Map<string, string | null>()
|
|
287
|
+
private exportResolutionCache = new Map<
|
|
288
|
+
string,
|
|
289
|
+
Map<string, { moduleInfo: ModuleInfo; binding: Binding } | null>
|
|
290
|
+
>()
|
|
237
291
|
// Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
|
|
238
292
|
// Maps: libName → (exportName → Kind)
|
|
239
293
|
// This allows O(1) resolution for the common case without async resolveId calls
|
|
@@ -246,11 +300,44 @@ export class ServerFnCompiler {
|
|
|
246
300
|
lookupKinds: Set<LookupKind>
|
|
247
301
|
loadModule: (id: string) => Promise<void>
|
|
248
302
|
resolveId: (id: string, importer?: string) => Promise<string | null>
|
|
303
|
+
/**
|
|
304
|
+
* In 'build' mode, resolution results are cached for performance.
|
|
305
|
+
* In 'dev' mode (default), caching is disabled to avoid invalidation complexity with HMR.
|
|
306
|
+
*/
|
|
307
|
+
mode?: 'dev' | 'build'
|
|
249
308
|
},
|
|
250
309
|
) {
|
|
251
310
|
this.validLookupKinds = options.lookupKinds
|
|
252
311
|
}
|
|
253
312
|
|
|
313
|
+
private get mode(): 'dev' | 'build' {
|
|
314
|
+
return this.options.mode ?? 'dev'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async resolveIdCached(id: string, importer?: string) {
|
|
318
|
+
if (this.mode === 'dev') {
|
|
319
|
+
return this.options.resolveId(id, importer)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const cacheKey = importer ? `${importer}::${id}` : id
|
|
323
|
+
const cached = this.resolveIdCache.get(cacheKey)
|
|
324
|
+
if (cached !== undefined) {
|
|
325
|
+
return cached
|
|
326
|
+
}
|
|
327
|
+
const resolved = await this.options.resolveId(id, importer)
|
|
328
|
+
this.resolveIdCache.set(cacheKey, resolved)
|
|
329
|
+
return resolved
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private getExportResolutionCache(moduleId: string) {
|
|
333
|
+
let cache = this.exportResolutionCache.get(moduleId)
|
|
334
|
+
if (!cache) {
|
|
335
|
+
cache = new Map()
|
|
336
|
+
this.exportResolutionCache.set(moduleId, cache)
|
|
337
|
+
}
|
|
338
|
+
return cache
|
|
339
|
+
}
|
|
340
|
+
|
|
254
341
|
private async init() {
|
|
255
342
|
// Register internal stub package exports for recognition.
|
|
256
343
|
// These don't need module resolution - only the knownRootImports fast path.
|
|
@@ -274,7 +361,18 @@ export class ServerFnCompiler {
|
|
|
274
361
|
}
|
|
275
362
|
libExports.set(config.rootExport, config.kind)
|
|
276
363
|
|
|
277
|
-
|
|
364
|
+
// For JSX lookups (e.g., ClientOnlyJSX), we only need the knownRootImports
|
|
365
|
+
// fast path to verify imports. Skip module resolution which may fail if
|
|
366
|
+
// the package isn't a direct dependency (e.g., @tanstack/react-router from
|
|
367
|
+
// within start-plugin-core).
|
|
368
|
+
if (config.kind !== 'Root') {
|
|
369
|
+
const setup = LookupSetup[config.kind]
|
|
370
|
+
if (setup.type === 'jsx') {
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const libId = await this.resolveIdCached(config.libName)
|
|
278
376
|
if (!libId) {
|
|
279
377
|
throw new Error(`could not resolve "${config.libName}"`)
|
|
280
378
|
}
|
|
@@ -311,9 +409,14 @@ export class ServerFnCompiler {
|
|
|
311
409
|
this.initialized = true
|
|
312
410
|
}
|
|
313
411
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Extracts bindings and exports from an already-parsed AST.
|
|
414
|
+
* This is the core logic shared by ingestModule and ingestModuleFromAst.
|
|
415
|
+
*/
|
|
416
|
+
private extractModuleInfo(
|
|
417
|
+
ast: ReturnType<typeof parseAst>,
|
|
418
|
+
id: string,
|
|
419
|
+
): ModuleInfo {
|
|
317
420
|
const bindings = new Map<string, Binding>()
|
|
318
421
|
const exports = new Map<string, ExportEntry>()
|
|
319
422
|
const reExportAllSources: Array<string> = []
|
|
@@ -414,10 +517,19 @@ export class ServerFnCompiler {
|
|
|
414
517
|
reExportAllSources,
|
|
415
518
|
}
|
|
416
519
|
this.moduleCache.set(id, info)
|
|
520
|
+
return info
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
public ingestModule({ code, id }: { code: string; id: string }) {
|
|
524
|
+
const ast = parseAst({ code })
|
|
525
|
+
const info = this.extractModuleInfo(ast, id)
|
|
417
526
|
return { info, ast }
|
|
418
527
|
}
|
|
419
528
|
|
|
420
529
|
public invalidateModule(id: string) {
|
|
530
|
+
// Note: Resolution caches (resolveIdCache, exportResolutionCache) are only
|
|
531
|
+
// used in build mode where there's no HMR. In dev mode, caching is disabled,
|
|
532
|
+
// so we only need to invalidate the moduleCache here.
|
|
421
533
|
return this.moduleCache.delete(id)
|
|
422
534
|
}
|
|
423
535
|
|
|
@@ -448,7 +560,13 @@ export class ServerFnCompiler {
|
|
|
448
560
|
}
|
|
449
561
|
|
|
450
562
|
const checkDirectCalls = needsDirectCallDetection(fileKinds)
|
|
563
|
+
// Optimization: ServerFn is always a top-level declaration (must be assigned to a variable).
|
|
564
|
+
// If the file only has ServerFn, we can skip full AST traversal and only visit
|
|
565
|
+
// the specific top-level declarations that have candidates.
|
|
566
|
+
const canUseFastPath = areAllKindsTopLevelOnly(fileKinds)
|
|
451
567
|
|
|
568
|
+
// Always parse and extract module info upfront.
|
|
569
|
+
// This ensures the module is cached for import resolution even if no candidates are found.
|
|
452
570
|
const { ast } = this.ingestModule({ code, id })
|
|
453
571
|
|
|
454
572
|
// Single-pass traversal to:
|
|
@@ -462,40 +580,145 @@ export class ServerFnCompiler {
|
|
|
462
580
|
babel.NodePath<t.CallExpression>
|
|
463
581
|
>()
|
|
464
582
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
583
|
+
// JSX candidates (e.g., <ClientOnly>)
|
|
584
|
+
const jsxCandidatePaths: Array<babel.NodePath<t.JSXElement>> = []
|
|
585
|
+
const checkJSX = needsJSXDetection(fileKinds)
|
|
586
|
+
// Get target component names from JSX setup (e.g., 'ClientOnly')
|
|
587
|
+
const jsxTargetComponentNames = checkJSX
|
|
588
|
+
? getJSXComponentNames(fileKinds)
|
|
589
|
+
: null
|
|
590
|
+
// Get module info that was just cached by ingestModule
|
|
591
|
+
const moduleInfo = this.moduleCache.get(id)!
|
|
592
|
+
|
|
593
|
+
if (canUseFastPath) {
|
|
594
|
+
// Fast path: only visit top-level statements that have potential candidates
|
|
595
|
+
|
|
596
|
+
// Collect indices of top-level statements that contain candidates
|
|
597
|
+
const candidateIndices: Array<number> = []
|
|
598
|
+
for (let i = 0; i < ast.program.body.length; i++) {
|
|
599
|
+
const node = ast.program.body[i]!
|
|
600
|
+
let declarations: Array<t.VariableDeclarator> | undefined
|
|
601
|
+
|
|
602
|
+
if (t.isVariableDeclaration(node)) {
|
|
603
|
+
declarations = node.declarations
|
|
604
|
+
} else if (t.isExportNamedDeclaration(node) && node.declaration) {
|
|
605
|
+
if (t.isVariableDeclaration(node.declaration)) {
|
|
606
|
+
declarations = node.declaration.declarations
|
|
607
|
+
}
|
|
479
608
|
}
|
|
480
609
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
610
|
+
if (declarations) {
|
|
611
|
+
for (const decl of declarations) {
|
|
612
|
+
if (decl.init && t.isCallExpression(decl.init)) {
|
|
613
|
+
if (isMethodChainCandidate(decl.init, fileKinds)) {
|
|
614
|
+
candidateIndices.push(i)
|
|
615
|
+
break // Only need to mark this statement once
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
485
619
|
}
|
|
620
|
+
}
|
|
486
621
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
622
|
+
// Early exit: no potential candidates found at top level
|
|
623
|
+
if (candidateIndices.length === 0) {
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Targeted traversal: only visit the specific statements that have candidates
|
|
628
|
+
// This is much faster than traversing the entire AST
|
|
629
|
+
babel.traverse(ast, {
|
|
630
|
+
Program(programPath) {
|
|
631
|
+
const bodyPaths = programPath.get('body')
|
|
632
|
+
for (const idx of candidateIndices) {
|
|
633
|
+
const stmtPath = bodyPaths[idx]
|
|
634
|
+
if (!stmtPath) continue
|
|
635
|
+
|
|
636
|
+
// Traverse only this statement's subtree
|
|
637
|
+
stmtPath.traverse({
|
|
638
|
+
CallExpression(path) {
|
|
639
|
+
const node = path.node
|
|
640
|
+
const parent = path.parent
|
|
641
|
+
|
|
642
|
+
// Check if this call is part of a larger chain (inner call)
|
|
643
|
+
if (
|
|
644
|
+
t.isMemberExpression(parent) &&
|
|
645
|
+
t.isCallExpression(path.parentPath.parent)
|
|
646
|
+
) {
|
|
647
|
+
chainCallPaths.set(node, path)
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Method chain pattern
|
|
652
|
+
if (isMethodChainCandidate(node, fileKinds)) {
|
|
653
|
+
candidatePaths.push(path)
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
// Stop traversal after processing Program
|
|
659
|
+
programPath.stop()
|
|
660
|
+
},
|
|
661
|
+
})
|
|
662
|
+
} else {
|
|
663
|
+
// Normal path: full traversal for non-fast-path kinds
|
|
664
|
+
babel.traverse(ast, {
|
|
665
|
+
CallExpression: (path) => {
|
|
666
|
+
const node = path.node
|
|
667
|
+
const parent = path.parent
|
|
668
|
+
|
|
669
|
+
// Check if this call is part of a larger chain (inner call)
|
|
670
|
+
// If so, store it for method chain lookup but don't treat as candidate
|
|
671
|
+
if (
|
|
672
|
+
t.isMemberExpression(parent) &&
|
|
673
|
+
t.isCallExpression(path.parentPath.parent)
|
|
674
|
+
) {
|
|
675
|
+
// This is an inner call in a chain - store for later lookup
|
|
676
|
+
chainCallPaths.set(node, path)
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.)
|
|
681
|
+
if (isMethodChainCandidate(node, fileKinds)) {
|
|
492
682
|
candidatePaths.push(path)
|
|
683
|
+
return
|
|
493
684
|
}
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
})
|
|
497
685
|
|
|
498
|
-
|
|
686
|
+
// Pattern 2: Direct call pattern
|
|
687
|
+
if (checkDirectCalls) {
|
|
688
|
+
if (isTopLevelDirectCallCandidate(path)) {
|
|
689
|
+
candidatePaths.push(path)
|
|
690
|
+
} else if (isNestedDirectCallCandidate(node)) {
|
|
691
|
+
candidatePaths.push(path)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
// Pattern 3: JSX element pattern (e.g., <ClientOnly>)
|
|
696
|
+
// Collect JSX elements where the component name matches a known import
|
|
697
|
+
// that resolves to a target component (e.g., ClientOnly from @tanstack/react-router)
|
|
698
|
+
JSXElement: (path) => {
|
|
699
|
+
if (!checkJSX || !jsxTargetComponentNames) return
|
|
700
|
+
|
|
701
|
+
const openingElement = path.node.openingElement
|
|
702
|
+
const nameNode = openingElement.name
|
|
703
|
+
|
|
704
|
+
// Only handle simple identifier names (not namespaced or member expressions)
|
|
705
|
+
if (!t.isJSXIdentifier(nameNode)) return
|
|
706
|
+
|
|
707
|
+
const componentName = nameNode.name
|
|
708
|
+
const binding = moduleInfo.bindings.get(componentName)
|
|
709
|
+
|
|
710
|
+
// Must be an import binding
|
|
711
|
+
if (!binding || binding.type !== 'import') return
|
|
712
|
+
|
|
713
|
+
// Check if the original import name matches a target component
|
|
714
|
+
if (jsxTargetComponentNames.has(binding.importedName)) {
|
|
715
|
+
jsxCandidatePaths.push(path)
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (candidatePaths.length === 0 && jsxCandidatePaths.length === 0) {
|
|
499
722
|
return null
|
|
500
723
|
}
|
|
501
724
|
|
|
@@ -512,7 +735,7 @@ export class ServerFnCompiler {
|
|
|
512
735
|
this.validLookupKinds.has(kind as LookupKind),
|
|
513
736
|
) as Array<{ path: babel.NodePath<t.CallExpression>; kind: LookupKind }>
|
|
514
737
|
|
|
515
|
-
if (validCandidates.length === 0) {
|
|
738
|
+
if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) {
|
|
516
739
|
return null
|
|
517
740
|
}
|
|
518
741
|
|
|
@@ -605,6 +828,29 @@ export class ServerFnCompiler {
|
|
|
605
828
|
}
|
|
606
829
|
}
|
|
607
830
|
|
|
831
|
+
// Handle JSX candidates (e.g., <ClientOnly>)
|
|
832
|
+
// Note: We only reach here on the server (ClientOnlyJSX is only in LookupKindsPerEnv.server)
|
|
833
|
+
// Verify import source using knownRootImports (same as function call resolution)
|
|
834
|
+
for (const jsxPath of jsxCandidatePaths) {
|
|
835
|
+
const openingElement = jsxPath.node.openingElement
|
|
836
|
+
const nameNode = openingElement.name
|
|
837
|
+
if (!t.isJSXIdentifier(nameNode)) continue
|
|
838
|
+
|
|
839
|
+
const componentName = nameNode.name
|
|
840
|
+
const binding = moduleInfo.bindings.get(componentName)
|
|
841
|
+
if (!binding || binding.type !== 'import') continue
|
|
842
|
+
|
|
843
|
+
// Verify the import source is a known TanStack router package
|
|
844
|
+
const knownExports = this.knownRootImports.get(binding.source)
|
|
845
|
+
if (!knownExports) continue
|
|
846
|
+
|
|
847
|
+
// Verify the imported name resolves to ClientOnlyJSX kind
|
|
848
|
+
const kind = knownExports.get(binding.importedName)
|
|
849
|
+
if (kind !== 'ClientOnlyJSX') continue
|
|
850
|
+
|
|
851
|
+
handleClientOnlyJSX(jsxPath, { env: 'server' })
|
|
852
|
+
}
|
|
853
|
+
|
|
608
854
|
deadCodeElimination(ast, refIdents)
|
|
609
855
|
|
|
610
856
|
return generateFromAst(ast, {
|
|
@@ -651,6 +897,19 @@ export class ServerFnCompiler {
|
|
|
651
897
|
exportName: string,
|
|
652
898
|
visitedModules = new Set<string>(),
|
|
653
899
|
): Promise<{ moduleInfo: ModuleInfo; binding: Binding } | undefined> {
|
|
900
|
+
const isBuildMode = this.mode === 'build'
|
|
901
|
+
|
|
902
|
+
// Check cache first (only for top-level calls in build mode)
|
|
903
|
+
if (isBuildMode && visitedModules.size === 0) {
|
|
904
|
+
const moduleCache = this.exportResolutionCache.get(moduleInfo.id)
|
|
905
|
+
if (moduleCache) {
|
|
906
|
+
const cached = moduleCache.get(exportName)
|
|
907
|
+
if (cached !== undefined) {
|
|
908
|
+
return cached ?? undefined
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
654
913
|
// Prevent infinite loops in circular re-exports
|
|
655
914
|
if (visitedModules.has(moduleInfo.id)) {
|
|
656
915
|
return undefined
|
|
@@ -662,7 +921,12 @@ export class ServerFnCompiler {
|
|
|
662
921
|
if (directExport) {
|
|
663
922
|
const binding = moduleInfo.bindings.get(directExport.name)
|
|
664
923
|
if (binding) {
|
|
665
|
-
|
|
924
|
+
const result = { moduleInfo, binding }
|
|
925
|
+
// Cache the result (build mode only)
|
|
926
|
+
if (isBuildMode) {
|
|
927
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, result)
|
|
928
|
+
}
|
|
929
|
+
return result
|
|
666
930
|
}
|
|
667
931
|
}
|
|
668
932
|
|
|
@@ -671,10 +935,11 @@ export class ServerFnCompiler {
|
|
|
671
935
|
if (moduleInfo.reExportAllSources.length > 0) {
|
|
672
936
|
const results = await Promise.all(
|
|
673
937
|
moduleInfo.reExportAllSources.map(async (reExportSource) => {
|
|
674
|
-
const reExportTarget = await this.
|
|
938
|
+
const reExportTarget = await this.resolveIdCached(
|
|
675
939
|
reExportSource,
|
|
676
940
|
moduleInfo.id,
|
|
677
941
|
)
|
|
942
|
+
|
|
678
943
|
if (reExportTarget) {
|
|
679
944
|
const reExportModule = await this.getModuleInfo(reExportTarget)
|
|
680
945
|
return this.findExportInModule(
|
|
@@ -689,11 +954,19 @@ export class ServerFnCompiler {
|
|
|
689
954
|
// Return the first valid result
|
|
690
955
|
for (const result of results) {
|
|
691
956
|
if (result) {
|
|
957
|
+
// Cache the result (build mode only)
|
|
958
|
+
if (isBuildMode) {
|
|
959
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, result)
|
|
960
|
+
}
|
|
692
961
|
return result
|
|
693
962
|
}
|
|
694
963
|
}
|
|
695
964
|
}
|
|
696
965
|
|
|
966
|
+
// Cache negative result (build mode only)
|
|
967
|
+
if (isBuildMode) {
|
|
968
|
+
this.getExportResolutionCache(moduleInfo.id).set(exportName, null)
|
|
969
|
+
}
|
|
697
970
|
return undefined
|
|
698
971
|
}
|
|
699
972
|
|
|
@@ -719,7 +992,7 @@ export class ServerFnCompiler {
|
|
|
719
992
|
}
|
|
720
993
|
|
|
721
994
|
// Slow path: resolve through the module graph
|
|
722
|
-
const target = await this.
|
|
995
|
+
const target = await this.resolveIdCached(binding.source, fileId)
|
|
723
996
|
if (!target) {
|
|
724
997
|
return 'None'
|
|
725
998
|
}
|
|
@@ -863,7 +1136,7 @@ export class ServerFnCompiler {
|
|
|
863
1136
|
binding.importedName === '*'
|
|
864
1137
|
) {
|
|
865
1138
|
// resolve the property from the target module
|
|
866
|
-
const targetModuleId = await this.
|
|
1139
|
+
const targetModuleId = await this.resolveIdCached(
|
|
867
1140
|
binding.source,
|
|
868
1141
|
fileId,
|
|
869
1142
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as t from '@babel/types'
|
|
2
|
+
import type * as babel from '@babel/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles <ClientOnly> JSX elements on the server side.
|
|
6
|
+
*
|
|
7
|
+
* On the server, the children of <ClientOnly> should be removed since they
|
|
8
|
+
* are client-only code. Only the fallback prop (if present) will be rendered.
|
|
9
|
+
*
|
|
10
|
+
* Transform:
|
|
11
|
+
* <ClientOnly fallback={<Loading />}>{clientOnlyContent}</ClientOnly>
|
|
12
|
+
* Into:
|
|
13
|
+
* <ClientOnly fallback={<Loading />} />
|
|
14
|
+
*
|
|
15
|
+
* Or if no fallback:
|
|
16
|
+
* <ClientOnly>{clientOnlyContent}</ClientOnly>
|
|
17
|
+
* Into:
|
|
18
|
+
* <ClientOnly />
|
|
19
|
+
*/
|
|
20
|
+
export function handleClientOnlyJSX(
|
|
21
|
+
path: babel.NodePath<t.JSXElement>,
|
|
22
|
+
_opts: { env: 'server' },
|
|
23
|
+
): void {
|
|
24
|
+
const element = path.node
|
|
25
|
+
|
|
26
|
+
// Remove all children - they are client-only code
|
|
27
|
+
element.children = []
|
|
28
|
+
|
|
29
|
+
// Make it a self-closing element since there are no children
|
|
30
|
+
element.openingElement.selfClosing = true
|
|
31
|
+
element.closingElement = null
|
|
32
|
+
}
|
|
@@ -75,7 +75,15 @@ const getLookupConfigurationsForEnv = (
|
|
|
75
75
|
...commonConfigs,
|
|
76
76
|
]
|
|
77
77
|
} else {
|
|
78
|
-
|
|
78
|
+
// Server-only: add ClientOnly JSX component lookup
|
|
79
|
+
return [
|
|
80
|
+
...commonConfigs,
|
|
81
|
+
{
|
|
82
|
+
libName: `@tanstack/${framework}-router`,
|
|
83
|
+
rootExport: 'ClientOnly',
|
|
84
|
+
kind: 'ClientOnlyJSX',
|
|
85
|
+
},
|
|
86
|
+
]
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
const SERVER_FN_LOOKUP = 'server-fn-module-lookup'
|
|
@@ -118,6 +126,9 @@ export function createServerFnPlugin(opts: {
|
|
|
118
126
|
async handler(code, id) {
|
|
119
127
|
let compiler = compilers[this.environment.name]
|
|
120
128
|
if (!compiler) {
|
|
129
|
+
// Default to 'dev' mode for unknown environments (conservative: no caching)
|
|
130
|
+
const mode =
|
|
131
|
+
this.environment.mode === 'build' ? 'build' : ('dev' as const)
|
|
121
132
|
compiler = new ServerFnCompiler({
|
|
122
133
|
env: environment.type,
|
|
123
134
|
directive: opts.directive,
|
|
@@ -126,6 +137,7 @@ export function createServerFnPlugin(opts: {
|
|
|
126
137
|
environment.type,
|
|
127
138
|
opts.framework,
|
|
128
139
|
),
|
|
140
|
+
mode,
|
|
129
141
|
loadModule: async (id: string) => {
|
|
130
142
|
if (this.environment.mode === 'build') {
|
|
131
143
|
const loaded = await this.load({ id })
|