@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.
@@ -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<LookupKind, MethodChainSetup | DirectCallSetup> = {
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 (setup.type === 'directCall' || setup.allowRootAsCandidate) {
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
- const libId = await this.options.resolveId(config.libName)
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
- public ingestModule({ code, id }: { code: string; id: string }) {
315
- const ast = parseAst({ code })
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
- babel.traverse(ast, {
466
- CallExpression: (path) => {
467
- const node = path.node
468
- const parent = path.parent
469
-
470
- // Check if this call is part of a larger chain (inner call)
471
- // If so, store it for method chain lookup but don't treat as candidate
472
- if (
473
- t.isMemberExpression(parent) &&
474
- t.isCallExpression(path.parentPath.parent)
475
- ) {
476
- // This is an inner call in a chain - store for later lookup
477
- chainCallPaths.set(node, path)
478
- return
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
- // Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.)
482
- if (isMethodChainCandidate(node, fileKinds)) {
483
- candidatePaths.push(path)
484
- return
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
- // Pattern 2: Direct call pattern
488
- if (checkDirectCalls) {
489
- if (isTopLevelDirectCallCandidate(path)) {
490
- candidatePaths.push(path)
491
- } else if (isNestedDirectCallCandidate(node)) {
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
- if (candidatePaths.length === 0) {
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
- return { moduleInfo, binding }
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.options.resolveId(
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.options.resolveId(binding.source, fileId)
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.options.resolveId(
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
- return commonConfigs
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 })