@tanstack/start-plugin-core 1.143.4 → 1.143.5

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.
Files changed (68) hide show
  1. package/dist/esm/plugin.js +11 -46
  2. package/dist/esm/plugin.js.map +1 -1
  3. package/dist/esm/{create-server-fn-plugin → start-compiler-plugin}/compiler.d.ts +39 -4
  4. package/dist/esm/{create-server-fn-plugin → start-compiler-plugin}/compiler.js +82 -24
  5. package/dist/esm/start-compiler-plugin/compiler.js.map +1 -0
  6. package/dist/esm/start-compiler-plugin/handleClientOnlyJSX.js.map +1 -0
  7. package/dist/esm/start-compiler-plugin/handleCreateIsomorphicFn.d.ts +8 -0
  8. package/dist/esm/start-compiler-plugin/handleCreateIsomorphicFn.js +33 -0
  9. package/dist/esm/start-compiler-plugin/handleCreateIsomorphicFn.js.map +1 -0
  10. package/dist/esm/start-compiler-plugin/handleCreateMiddleware.d.ts +8 -0
  11. package/dist/esm/start-compiler-plugin/handleCreateMiddleware.js +25 -0
  12. package/dist/esm/start-compiler-plugin/handleCreateMiddleware.js.map +1 -0
  13. package/dist/esm/start-compiler-plugin/handleCreateServerFn.d.ts +19 -0
  14. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +262 -0
  15. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -0
  16. package/dist/esm/start-compiler-plugin/handleEnvOnly.d.ts +10 -0
  17. package/dist/esm/start-compiler-plugin/handleEnvOnly.js +38 -0
  18. package/dist/esm/start-compiler-plugin/handleEnvOnly.js.map +1 -0
  19. package/dist/esm/start-compiler-plugin/plugin.d.ts +19 -0
  20. package/dist/esm/start-compiler-plugin/plugin.js +314 -0
  21. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -0
  22. package/dist/esm/start-compiler-plugin/types.d.ts +116 -0
  23. package/dist/esm/start-compiler-plugin/utils.d.ts +23 -0
  24. package/dist/esm/start-compiler-plugin/utils.js +34 -0
  25. package/dist/esm/start-compiler-plugin/utils.js.map +1 -0
  26. package/dist/esm/types.d.ts +0 -1
  27. package/package.json +3 -4
  28. package/src/plugin.ts +10 -50
  29. package/src/{create-server-fn-plugin → start-compiler-plugin}/compiler.ts +162 -30
  30. package/src/start-compiler-plugin/handleCreateIsomorphicFn.ts +54 -0
  31. package/src/start-compiler-plugin/handleCreateMiddleware.ts +39 -0
  32. package/src/start-compiler-plugin/handleCreateServerFn.ts +491 -0
  33. package/src/start-compiler-plugin/handleEnvOnly.ts +56 -0
  34. package/src/start-compiler-plugin/plugin.ts +423 -0
  35. package/src/start-compiler-plugin/types.ts +133 -0
  36. package/src/start-compiler-plugin/utils.ts +52 -0
  37. package/src/types.ts +0 -1
  38. package/dist/esm/create-server-fn-plugin/compiler.js.map +0 -1
  39. package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.js.map +0 -1
  40. package/dist/esm/create-server-fn-plugin/handleCreateIsomorphicFn.d.ts +0 -4
  41. package/dist/esm/create-server-fn-plugin/handleCreateIsomorphicFn.js +0 -31
  42. package/dist/esm/create-server-fn-plugin/handleCreateIsomorphicFn.js.map +0 -1
  43. package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.d.ts +0 -10
  44. package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.js +0 -29
  45. package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.js.map +0 -1
  46. package/dist/esm/create-server-fn-plugin/handleCreateServerFn.d.ts +0 -17
  47. package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js +0 -82
  48. package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js.map +0 -1
  49. package/dist/esm/create-server-fn-plugin/handleEnvOnly.d.ts +0 -6
  50. package/dist/esm/create-server-fn-plugin/handleEnvOnly.js +0 -36
  51. package/dist/esm/create-server-fn-plugin/handleEnvOnly.js.map +0 -1
  52. package/dist/esm/create-server-fn-plugin/plugin.d.ts +0 -10
  53. package/dist/esm/create-server-fn-plugin/plugin.js +0 -186
  54. package/dist/esm/create-server-fn-plugin/plugin.js.map +0 -1
  55. package/dist/esm/create-server-fn-plugin/types.d.ts +0 -30
  56. package/dist/esm/create-server-fn-plugin/utils.d.ts +0 -10
  57. package/dist/esm/create-server-fn-plugin/utils.js +0 -19
  58. package/dist/esm/create-server-fn-plugin/utils.js.map +0 -1
  59. package/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts +0 -46
  60. package/src/create-server-fn-plugin/handleCreateMiddleware.ts +0 -45
  61. package/src/create-server-fn-plugin/handleCreateServerFn.ts +0 -145
  62. package/src/create-server-fn-plugin/handleEnvOnly.ts +0 -45
  63. package/src/create-server-fn-plugin/plugin.ts +0 -234
  64. package/src/create-server-fn-plugin/types.ts +0 -34
  65. package/src/create-server-fn-plugin/utils.ts +0 -24
  66. /package/dist/esm/{create-server-fn-plugin → start-compiler-plugin}/handleClientOnlyJSX.d.ts +0 -0
  67. /package/dist/esm/{create-server-fn-plugin → start-compiler-plugin}/handleClientOnlyJSX.js +0 -0
  68. /package/src/{create-server-fn-plugin → start-compiler-plugin}/handleClientOnlyJSX.ts +0 -0
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable import/no-commonjs */
2
+ import crypto from 'node:crypto'
2
3
  import * as t from '@babel/types'
3
4
  import { generateFromAst, parseAst } from '@tanstack/router-utils'
4
5
  import babel from '@babel/core'
@@ -11,7 +12,13 @@ import { handleCreateMiddleware } from './handleCreateMiddleware'
11
12
  import { handleCreateIsomorphicFn } from './handleCreateIsomorphicFn'
12
13
  import { handleEnvOnlyFn } from './handleEnvOnly'
13
14
  import { handleClientOnlyJSX } from './handleClientOnlyJSX'
14
- import type { MethodChainPaths, RewriteCandidate } from './types'
15
+ import type {
16
+ CompilationContext,
17
+ MethodChainPaths,
18
+ RewriteCandidate,
19
+ ServerFn,
20
+ } from './types'
21
+ import type { CompileStartFrameworkOptions } from '../types'
15
22
 
16
23
  type Binding =
17
24
  | {
@@ -106,6 +113,32 @@ export const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>> = {
106
113
  ] as const),
107
114
  }
108
115
 
116
+ /**
117
+ * Handler type for processing candidates of a specific kind.
118
+ * The kind is passed as the third argument to allow shared handlers (like handleEnvOnlyFn).
119
+ */
120
+ type KindHandler = (
121
+ candidates: Array<RewriteCandidate>,
122
+ context: CompilationContext,
123
+ kind: LookupKind,
124
+ ) => void
125
+
126
+ /**
127
+ * Registry mapping each LookupKind to its handler function.
128
+ * When adding a new kind, add its handler here.
129
+ */
130
+ const KindHandlers: Record<
131
+ Exclude<LookupKind, 'ClientOnlyJSX'>,
132
+ KindHandler
133
+ > = {
134
+ ServerFn: handleCreateServerFn,
135
+ Middleware: handleCreateMiddleware,
136
+ IsomorphicFn: handleCreateIsomorphicFn,
137
+ ServerOnlyFn: handleEnvOnlyFn,
138
+ ClientOnlyFn: handleEnvOnlyFn,
139
+ // ClientOnlyJSX is handled separately via JSX traversal, not here
140
+ }
141
+
109
142
  /**
110
143
  * Detects which LookupKinds are present in the code using string matching.
111
144
  * This is a fast pre-scan before AST parsing to limit the work done during compilation.
@@ -279,7 +312,7 @@ function isTopLevelDirectCallCandidate(
279
312
  return t.isProgram(path.parentPath.parentPath?.parent)
280
313
  }
281
314
 
282
- export class ServerFnCompiler {
315
+ export class StartCompiler {
283
316
  private moduleCache = new Map<string, ModuleInfo>()
284
317
  private initialized = false
285
318
  private validLookupKinds: Set<LookupKind>
@@ -292,10 +325,16 @@ export class ServerFnCompiler {
292
325
  // Maps: libName → (exportName → Kind)
293
326
  // This allows O(1) resolution for the common case without async resolveId calls
294
327
  private knownRootImports = new Map<string, Map<string, Kind>>()
328
+
329
+ // For generating unique function IDs in production builds
330
+ private entryIdToFunctionId = new Map<string, string>()
331
+ private functionIds = new Set<string>()
332
+
295
333
  constructor(
296
334
  private options: {
297
335
  env: 'client' | 'server'
298
- directive: string
336
+ envName: string
337
+ root: string
299
338
  lookupConfigurations: Array<LookupConfig>
300
339
  lookupKinds: Set<LookupKind>
301
340
  loadModule: (id: string) => Promise<void>
@@ -305,11 +344,92 @@ export class ServerFnCompiler {
305
344
  * In 'dev' mode (default), caching is disabled to avoid invalidation complexity with HMR.
306
345
  */
307
346
  mode?: 'dev' | 'build'
347
+ /**
348
+ * The framework being used (e.g., 'react', 'solid').
349
+ */
350
+ framework: CompileStartFrameworkOptions
351
+ /**
352
+ * The Vite environment name for the server function provider.
353
+ */
354
+ providerEnvName: string
355
+ /**
356
+ * Custom function ID generator (optional, defaults to hash-based).
357
+ */
358
+ generateFunctionId?: (opts: {
359
+ filename: string
360
+ functionName: string
361
+ }) => string | undefined
362
+ /**
363
+ * Callback when server functions are discovered.
364
+ * Called after each file is compiled with its new functions.
365
+ */
366
+ onServerFnsById?: (d: Record<string, ServerFn>) => void
367
+ /**
368
+ * Returns the currently known server functions from previous builds.
369
+ * Used by server callers to look up canonical extracted filenames.
370
+ */
371
+ getKnownServerFns?: () => Record<string, ServerFn>
308
372
  },
309
373
  ) {
310
374
  this.validLookupKinds = options.lookupKinds
311
375
  }
312
376
 
377
+ /**
378
+ * Generates a unique function ID for a server function.
379
+ * In dev mode, uses a base64-encoded JSON with file path and export name.
380
+ * In build mode, uses SHA256 hash or custom generator.
381
+ */
382
+ private generateFunctionId(opts: {
383
+ filename: string
384
+ functionName: string
385
+ extractedFilename: string
386
+ }): string {
387
+ if (this.mode === 'dev') {
388
+ // In dev, encode the file path and export name for direct lookup
389
+ const rootWithTrailingSlash = this.options.root.endsWith('/')
390
+ ? this.options.root
391
+ : `${this.options.root}/`
392
+ let file = opts.extractedFilename
393
+ if (opts.extractedFilename.startsWith(rootWithTrailingSlash)) {
394
+ file = opts.extractedFilename.slice(rootWithTrailingSlash.length)
395
+ }
396
+ file = `/@id/${file}`
397
+
398
+ const serverFn = {
399
+ file,
400
+ export: opts.functionName,
401
+ }
402
+ return Buffer.from(JSON.stringify(serverFn), 'utf8').toString('base64url')
403
+ }
404
+
405
+ // Production build: use custom generator or hash
406
+ const entryId = `${opts.filename}--${opts.functionName}`
407
+ let functionId = this.entryIdToFunctionId.get(entryId)
408
+ if (functionId === undefined) {
409
+ if (this.options.generateFunctionId) {
410
+ functionId = this.options.generateFunctionId({
411
+ filename: opts.filename,
412
+ functionName: opts.functionName,
413
+ })
414
+ }
415
+ if (!functionId) {
416
+ functionId = crypto.createHash('sha256').update(entryId).digest('hex')
417
+ }
418
+ // Deduplicate in case the generated id conflicts with an existing id
419
+ if (this.functionIds.has(functionId)) {
420
+ let deduplicatedId
421
+ let iteration = 0
422
+ do {
423
+ deduplicatedId = `${functionId}_${++iteration}`
424
+ } while (this.functionIds.has(deduplicatedId))
425
+ functionId = deduplicatedId
426
+ }
427
+ this.entryIdToFunctionId.set(entryId, functionId)
428
+ this.functionIds.add(functionId)
429
+ }
430
+ return functionId
431
+ }
432
+
313
433
  private get mode(): 'dev' | 'build' {
314
434
  return this.options.mode ?? 'dev'
315
435
  }
@@ -536,12 +656,10 @@ export class ServerFnCompiler {
536
656
  public async compile({
537
657
  code,
538
658
  id,
539
- isProviderFile,
540
659
  detectedKinds,
541
660
  }: {
542
661
  code: string
543
662
  id: string
544
- isProviderFile: boolean
545
663
  /** Pre-detected kinds present in this file. If not provided, all valid kinds are checked. */
546
664
  detectedKinds?: Set<LookupKind>
547
665
  }) {
@@ -732,8 +850,11 @@ export class ServerFnCompiler {
732
850
 
733
851
  // Filter to valid candidates
734
852
  const validCandidates = resolvedCandidates.filter(({ kind }) =>
735
- this.validLookupKinds.has(kind as LookupKind),
736
- ) as Array<{ path: babel.NodePath<t.CallExpression>; kind: LookupKind }>
853
+ this.validLookupKinds.has(kind as Exclude<LookupKind, 'ClientOnlyJSX'>),
854
+ ) as Array<{
855
+ path: babel.NodePath<t.CallExpression>
856
+ kind: Exclude<LookupKind, 'ClientOnlyJSX'>
857
+ }>
737
858
 
738
859
  if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) {
739
860
  return null
@@ -742,7 +863,7 @@ export class ServerFnCompiler {
742
863
  // Process valid candidates to collect method chains
743
864
  const pathsToRewrite: Array<{
744
865
  path: babel.NodePath<t.CallExpression>
745
- kind: LookupKind
866
+ kind: Exclude<LookupKind, 'ClientOnlyJSX'>
746
867
  methodChain: MethodChainPaths
747
868
  }> = []
748
869
 
@@ -802,32 +923,43 @@ export class ServerFnCompiler {
802
923
 
803
924
  const refIdents = findReferencedIdentifiers(ast)
804
925
 
805
- for (const { path, kind, methodChain } of pathsToRewrite) {
806
- const candidate: RewriteCandidate = { path, methodChain }
807
- if (kind === 'ServerFn') {
808
- handleCreateServerFn(candidate, {
809
- env: this.options.env,
810
- code,
811
- directive: this.options.directive,
812
- isProviderFile,
813
- })
814
- } else if (kind === 'Middleware') {
815
- handleCreateMiddleware(candidate, {
816
- env: this.options.env,
817
- })
818
- } else if (kind === 'IsomorphicFn') {
819
- handleCreateIsomorphicFn(candidate, {
820
- env: this.options.env,
821
- })
926
+ const context: CompilationContext = {
927
+ ast,
928
+ id,
929
+ code,
930
+ env: this.options.env,
931
+ envName: this.options.envName,
932
+ root: this.options.root,
933
+ framework: this.options.framework,
934
+ providerEnvName: this.options.providerEnvName,
935
+
936
+ generateFunctionId: (opts) => this.generateFunctionId(opts),
937
+ getKnownServerFns: () => this.options.getKnownServerFns?.() ?? {},
938
+ onServerFnsById: this.options.onServerFnsById,
939
+ }
940
+
941
+ // Group candidates by kind for batch processing
942
+ const candidatesByKind = new Map<
943
+ Exclude<LookupKind, 'ClientOnlyJSX'>,
944
+ Array<RewriteCandidate>
945
+ >()
946
+
947
+ for (const { path: candidatePath, kind, methodChain } of pathsToRewrite) {
948
+ const candidate: RewriteCandidate = { path: candidatePath, methodChain }
949
+ const existing = candidatesByKind.get(kind)
950
+ if (existing) {
951
+ existing.push(candidate)
822
952
  } else {
823
- // ServerOnlyFn or ClientOnlyFn
824
- handleEnvOnlyFn(candidate, {
825
- env: this.options.env,
826
- kind,
827
- })
953
+ candidatesByKind.set(kind, [candidate])
828
954
  }
829
955
  }
830
956
 
957
+ // Process each kind using its registered handler
958
+ for (const [kind, candidates] of candidatesByKind) {
959
+ const handler = KindHandlers[kind]
960
+ handler(candidates, context, kind)
961
+ }
962
+
831
963
  // Handle JSX candidates (e.g., <ClientOnly>)
832
964
  // Note: We only reach here on the server (ClientOnlyJSX is only in LookupKindsPerEnv.server)
833
965
  // Verify import source using knownRootImports (same as function call resolution)
@@ -0,0 +1,54 @@
1
+ import * as t from '@babel/types'
2
+ import type { CompilationContext, RewriteCandidate } from './types'
3
+
4
+ /**
5
+ * Handles createIsomorphicFn transformations for a batch of candidates.
6
+ *
7
+ * @param candidates - All IsomorphicFn candidates to process
8
+ * @param context - The compilation context
9
+ */
10
+ export function handleCreateIsomorphicFn(
11
+ candidates: Array<RewriteCandidate>,
12
+ context: CompilationContext,
13
+ ): void {
14
+ for (const candidate of candidates) {
15
+ const { path, methodChain } = candidate
16
+
17
+ // Get the environment-specific call (.client() or .server())
18
+ const envCallInfo =
19
+ context.env === 'client' ? methodChain.client : methodChain.server
20
+
21
+ // Check if we have any implementation at all
22
+ if (!methodChain.client && !methodChain.server) {
23
+ // No implementations provided - warn and replace with no-op
24
+ const variableId = path.parentPath.isVariableDeclarator()
25
+ ? path.parentPath.node.id
26
+ : null
27
+ console.warn(
28
+ 'createIsomorphicFn called without a client or server implementation!',
29
+ 'This will result in a no-op function.',
30
+ 'Variable name:',
31
+ t.isIdentifier(variableId) ? variableId.name : 'unknown',
32
+ )
33
+ path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([])))
34
+ continue
35
+ }
36
+
37
+ if (!envCallInfo) {
38
+ // No implementation for this environment - replace with no-op
39
+ path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([])))
40
+ continue
41
+ }
42
+
43
+ // Extract the function argument from the environment-specific call
44
+ const innerFn = envCallInfo.firstArgPath?.node
45
+
46
+ if (!t.isExpression(innerFn)) {
47
+ throw new Error(
48
+ `createIsomorphicFn().${context.env}(func) must be called with a function!`,
49
+ )
50
+ }
51
+
52
+ path.replaceWith(innerFn)
53
+ }
54
+ }
@@ -0,0 +1,39 @@
1
+ import { stripMethodCall } from './utils'
2
+ import type { CompilationContext, RewriteCandidate } from './types'
3
+
4
+ /**
5
+ * Handles createMiddleware transformations for a batch of candidates.
6
+ *
7
+ * @param candidates - All Middleware candidates to process
8
+ * @param context - The compilation context
9
+ */
10
+ export function handleCreateMiddleware(
11
+ candidates: Array<RewriteCandidate>,
12
+ context: CompilationContext,
13
+ ): void {
14
+ if (context.env === 'server') {
15
+ throw new Error('handleCreateMiddleware should not be called on the server')
16
+ }
17
+
18
+ for (const candidate of candidates) {
19
+ const { inputValidator, server } = candidate.methodChain
20
+
21
+ if (inputValidator) {
22
+ const innerInputExpression = inputValidator.callPath.node.arguments[0]
23
+
24
+ if (!innerInputExpression) {
25
+ throw new Error(
26
+ 'createMiddleware().inputValidator() must be called with a validator!',
27
+ )
28
+ }
29
+
30
+ // remove the validator call expression
31
+ stripMethodCall(inputValidator.callPath)
32
+ }
33
+
34
+ if (server) {
35
+ // remove the server call expression
36
+ stripMethodCall(server.callPath)
37
+ }
38
+ }
39
+ }