@tanstack/start-plugin-core 1.132.0-alpha.7 → 1.132.0-alpha.9

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 (29) hide show
  1. package/dist/esm/create-server-fn-plugin/compiler.d.ts +61 -0
  2. package/dist/esm/create-server-fn-plugin/compiler.js +336 -0
  3. package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -0
  4. package/dist/esm/create-server-fn-plugin/handleCreateServerFn.d.ts +6 -0
  5. package/dist/esm/{start-compiler-plugin/serverFn.js → create-server-fn-plugin/handleCreateServerFn.js} +11 -13
  6. package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js.map +1 -0
  7. package/dist/esm/create-server-fn-plugin/plugin.d.ts +3 -0
  8. package/dist/esm/create-server-fn-plugin/plugin.js +113 -0
  9. package/dist/esm/create-server-fn-plugin/plugin.js.map +1 -0
  10. package/dist/esm/plugin.js +3 -4
  11. package/dist/esm/plugin.js.map +1 -1
  12. package/dist/esm/start-compiler-plugin/compilers.js +0 -6
  13. package/dist/esm/start-compiler-plugin/compilers.js.map +1 -1
  14. package/dist/esm/start-compiler-plugin/constants.d.ts +1 -1
  15. package/dist/esm/start-compiler-plugin/constants.js +0 -1
  16. package/dist/esm/start-compiler-plugin/constants.js.map +1 -1
  17. package/dist/esm/start-compiler-plugin/plugin.d.ts +1 -8
  18. package/dist/esm/start-compiler-plugin/plugin.js +6 -13
  19. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
  20. package/package.json +7 -7
  21. package/src/create-server-fn-plugin/compiler.ts +456 -0
  22. package/src/{start-compiler-plugin/serverFn.ts → create-server-fn-plugin/handleCreateServerFn.ts} +26 -36
  23. package/src/create-server-fn-plugin/plugin.ts +138 -0
  24. package/src/plugin.ts +3 -4
  25. package/src/start-compiler-plugin/compilers.ts +0 -6
  26. package/src/start-compiler-plugin/constants.ts +0 -1
  27. package/src/start-compiler-plugin/plugin.ts +7 -22
  28. package/dist/esm/start-compiler-plugin/serverFn.d.ts +0 -4
  29. package/dist/esm/start-compiler-plugin/serverFn.js.map +0 -1
@@ -0,0 +1,456 @@
1
+ /* eslint-disable import/no-commonjs */
2
+ import * as t from '@babel/types'
3
+ import { generateFromAst, parseAst } from '@tanstack/router-utils'
4
+ import babel from '@babel/core'
5
+ import {
6
+ deadCodeElimination,
7
+ findReferencedIdentifiers,
8
+ } from 'babel-dead-code-elimination'
9
+ import { handleCreateServerFn } from './handleCreateServerFn'
10
+
11
+ type Binding =
12
+ | {
13
+ type: 'import'
14
+ source: string
15
+ importedName: string
16
+ resolvedKind?: Kind
17
+ }
18
+ | {
19
+ type: 'var'
20
+ init: t.Expression | null
21
+ resolvedKind?: Kind
22
+ }
23
+
24
+ type ExportEntry =
25
+ | { tag: 'Normal'; name: string }
26
+ | { tag: 'Default'; name: string }
27
+ | { tag: 'Namespace'; name: string; targetId: string } // for `export * as ns from './x'`
28
+
29
+ type Kind = 'None' | 'Root' | 'Builder' | 'ServerFn'
30
+
31
+ interface ModuleInfo {
32
+ id: string
33
+ code: string
34
+ ast: ReturnType<typeof parseAst>
35
+ bindings: Map<string, Binding>
36
+ exports: Map<string, ExportEntry>
37
+ }
38
+
39
+ export class ServerFnCompiler {
40
+ private moduleCache = new Map<string, ModuleInfo>()
41
+ private resolvedLibId!: string
42
+ private initialized = false
43
+ constructor(
44
+ private options: {
45
+ env: 'client' | 'server'
46
+ libName: string
47
+ rootExport: string
48
+ loadModule: (id: string) => Promise<void>
49
+ resolveId: (id: string, importer?: string) => Promise<string | null>
50
+ },
51
+ ) {}
52
+
53
+ private async init(id: string) {
54
+ const libId = await this.options.resolveId(this.options.libName, id)
55
+ if (!libId) {
56
+ throw new Error(`could not resolve "${this.options.libName}"`)
57
+ }
58
+ // insert root binding
59
+ const rootModule = {
60
+ ast: null as any,
61
+ bindings: new Map(),
62
+ exports: new Map(),
63
+ code: '',
64
+ id: libId,
65
+ }
66
+ rootModule.exports.set(this.options.rootExport, {
67
+ tag: 'Normal',
68
+ name: this.options.rootExport,
69
+ })
70
+ rootModule.bindings.set(this.options.rootExport, {
71
+ type: 'var',
72
+ init: t.identifier(this.options.rootExport),
73
+ resolvedKind: 'Root',
74
+ })
75
+ this.moduleCache.set(libId, rootModule)
76
+ this.initialized = true
77
+ this.resolvedLibId = libId
78
+ }
79
+
80
+ public ingestModule({ code, id }: { code: string; id: string }) {
81
+ const ast = parseAst({ code })
82
+
83
+ const bindings = new Map<string, Binding>()
84
+ const exports = new Map<string, ExportEntry>()
85
+
86
+ // we are only interested in top-level bindings, hence we don't traverse the AST
87
+ // instead we only iterate over the program body
88
+ for (const node of ast.program.body) {
89
+ if (t.isImportDeclaration(node)) {
90
+ const source = node.source.value
91
+ for (const s of node.specifiers) {
92
+ if (t.isImportSpecifier(s)) {
93
+ const importedName = t.isIdentifier(s.imported)
94
+ ? s.imported.name
95
+ : s.imported.value
96
+ bindings.set(s.local.name, { type: 'import', source, importedName })
97
+ } else if (t.isImportDefaultSpecifier(s)) {
98
+ bindings.set(s.local.name, {
99
+ type: 'import',
100
+ source,
101
+ importedName: 'default',
102
+ })
103
+ } else if (t.isImportNamespaceSpecifier(s)) {
104
+ bindings.set(s.local.name, {
105
+ type: 'import',
106
+ source,
107
+ importedName: '*',
108
+ })
109
+ }
110
+ }
111
+ } else if (t.isVariableDeclaration(node)) {
112
+ for (const decl of node.declarations) {
113
+ if (t.isIdentifier(decl.id)) {
114
+ bindings.set(decl.id.name, {
115
+ type: 'var',
116
+ init: decl.init ?? null,
117
+ })
118
+ }
119
+ }
120
+ } else if (t.isExportNamedDeclaration(node)) {
121
+ // export const foo = ...
122
+ if (node.declaration) {
123
+ if (t.isVariableDeclaration(node.declaration)) {
124
+ for (const d of node.declaration.declarations) {
125
+ if (t.isIdentifier(d.id)) {
126
+ exports.set(d.id.name, { tag: 'Normal', name: d.id.name })
127
+ bindings.set(d.id.name, { type: 'var', init: d.init ?? null })
128
+ }
129
+ }
130
+ }
131
+ }
132
+ for (const sp of node.specifiers) {
133
+ if (t.isExportNamespaceSpecifier(sp)) {
134
+ exports.set(sp.exported.name, {
135
+ tag: 'Namespace',
136
+ name: sp.exported.name,
137
+ targetId: node.source?.value || '',
138
+ })
139
+ }
140
+ // export { local as exported }
141
+ else if (t.isExportSpecifier(sp)) {
142
+ const local = sp.local.name
143
+ const exported = t.isIdentifier(sp.exported)
144
+ ? sp.exported.name
145
+ : sp.exported.value
146
+ exports.set(exported, { tag: 'Normal', name: local })
147
+ }
148
+ }
149
+ } else if (t.isExportDefaultDeclaration(node)) {
150
+ const d = node.declaration
151
+ if (t.isIdentifier(d)) {
152
+ exports.set('default', { tag: 'Default', name: d.name })
153
+ } else {
154
+ const synth = '__default_export__'
155
+ bindings.set(synth, { type: 'var', init: d as t.Expression })
156
+ exports.set('default', { tag: 'Default', name: synth })
157
+ }
158
+ }
159
+ }
160
+
161
+ const info: ModuleInfo = { code, id, ast, bindings, exports }
162
+ this.moduleCache.set(id, info)
163
+ return info
164
+ }
165
+
166
+ public invalidateModule(id: string) {
167
+ return this.moduleCache.delete(id)
168
+ }
169
+
170
+ public async compile({ code, id }: { code: string; id: string }) {
171
+ if (!this.initialized) {
172
+ await this.init(id)
173
+ }
174
+ const { bindings, ast } = this.ingestModule({ code, id })
175
+ const candidates = this.collectHandlerCandidates(bindings)
176
+ if (candidates.length === 0) {
177
+ // this hook will only be invoked if there is `.handler(` in the code,
178
+ // so not discovering a handler candidate is rather unlikely, but maybe possible?
179
+ return null
180
+ }
181
+
182
+ // let's find out which of the candidates are actually server functions
183
+ const toRewrite: Array<t.CallExpression> = []
184
+ for (const handler of candidates) {
185
+ const kind = await this.resolveExprKind(handler, id)
186
+ if (kind === 'ServerFn') {
187
+ toRewrite.push(handler)
188
+ }
189
+ }
190
+ if (toRewrite.length === 0) {
191
+ return null
192
+ }
193
+ const pathsToRewrite: Array<babel.NodePath<t.CallExpression>> = []
194
+ babel.traverse(ast, {
195
+ CallExpression(path) {
196
+ const found = toRewrite.findIndex((h) => path.node === h)
197
+ if (found !== -1) {
198
+ pathsToRewrite.push(path)
199
+ // delete from toRewrite
200
+ toRewrite.splice(found, 1)
201
+ }
202
+ },
203
+ })
204
+
205
+ if (toRewrite.length > 0) {
206
+ throw new Error(
207
+ `Internal error: could not find all paths to rewrite. please file an issue`,
208
+ )
209
+ }
210
+
211
+ const refIdents = findReferencedIdentifiers(ast)
212
+
213
+ pathsToRewrite.map((p) =>
214
+ handleCreateServerFn(p, { env: this.options.env, code }),
215
+ )
216
+
217
+ deadCodeElimination(ast, refIdents)
218
+
219
+ return generateFromAst(ast, {
220
+ sourceMaps: true,
221
+ sourceFileName: id,
222
+ filename: id,
223
+ })
224
+ }
225
+
226
+ // collects all `.handler(...)` CallExpressions at top-level
227
+ private collectHandlerCandidates(bindings: Map<string, Binding>) {
228
+ const candidates: Array<t.CallExpression> = []
229
+
230
+ for (const binding of bindings.values()) {
231
+ if (binding.type === 'var') {
232
+ const handler = isHandlerCall(binding.init)
233
+ if (handler) {
234
+ candidates.push(handler)
235
+ }
236
+ }
237
+ }
238
+ return candidates
239
+ }
240
+
241
+ private async resolveIdentifierKind(
242
+ ident: string,
243
+ id: string,
244
+ visited = new Set<string>(),
245
+ ): Promise<Kind> {
246
+ const info = await this.getModuleInfo(id)
247
+
248
+ const binding = info.bindings.get(ident)
249
+ if (!binding) {
250
+ return 'None'
251
+ }
252
+ if (binding.resolvedKind) {
253
+ return binding.resolvedKind
254
+ }
255
+
256
+ // TODO improve cycle detection? should we throw here instead of returning 'None'?
257
+ // prevent cycles
258
+ const vKey = `${id}:${ident}`
259
+ if (visited.has(vKey)) {
260
+ return 'None'
261
+ }
262
+ visited.add(vKey)
263
+
264
+ const resolvedKind = await this.resolveBindingKind(binding, id, visited)
265
+ binding.resolvedKind = resolvedKind
266
+ return resolvedKind
267
+ }
268
+
269
+ private async resolveBindingKind(
270
+ binding: Binding,
271
+ fileId: string,
272
+ visited = new Set<string>(),
273
+ ): Promise<Kind> {
274
+ if (binding.resolvedKind) {
275
+ return binding.resolvedKind
276
+ }
277
+ if (binding.type === 'import') {
278
+ const target = await this.options.resolveId(binding.source, fileId)
279
+ if (!target) {
280
+ return 'None'
281
+ }
282
+
283
+ if (binding.importedName === '*') {
284
+ throw new Error(
285
+ `should never get here, namespace imports are handled in resolveCalleeKind`,
286
+ )
287
+ }
288
+
289
+ const importedModule = await this.getModuleInfo(target)
290
+
291
+ const moduleExport = importedModule.exports.get(binding.importedName)
292
+ if (!moduleExport) {
293
+ return 'None'
294
+ }
295
+ const importedBinding = importedModule.bindings.get(moduleExport.name)
296
+ if (!importedBinding) {
297
+ return 'None'
298
+ }
299
+ if (importedBinding.resolvedKind) {
300
+ return importedBinding.resolvedKind
301
+ }
302
+
303
+ const resolvedKind = await this.resolveBindingKind(
304
+ importedBinding,
305
+ importedModule.id,
306
+ visited,
307
+ )
308
+ importedBinding.resolvedKind = resolvedKind
309
+ return resolvedKind
310
+ }
311
+
312
+ const resolvedKind = await this.resolveExprKind(
313
+ binding.init,
314
+ fileId,
315
+ visited,
316
+ )
317
+ binding.resolvedKind = resolvedKind
318
+ return resolvedKind
319
+ }
320
+
321
+ private async resolveExprKind(
322
+ expr: t.Expression | null,
323
+ fileId: string,
324
+ visited = new Set<string>(),
325
+ ): Promise<Kind> {
326
+ if (!expr) {
327
+ return 'None'
328
+ }
329
+
330
+ let result: Kind = 'None'
331
+
332
+ if (t.isCallExpression(expr)) {
333
+ if (!t.isExpression(expr.callee)) {
334
+ return 'None'
335
+ }
336
+ const calleeKind = await this.resolveCalleeKind(
337
+ expr.callee,
338
+ fileId,
339
+ visited,
340
+ )
341
+ if (calleeKind === 'Root' || calleeKind === 'Builder') {
342
+ return 'Builder'
343
+ }
344
+ if (calleeKind === 'ServerFn') {
345
+ return 'ServerFn'
346
+ }
347
+ } else if (t.isMemberExpression(expr) && t.isIdentifier(expr.property)) {
348
+ result = await this.resolveCalleeKind(expr.object, fileId, visited)
349
+ }
350
+
351
+ if (result === 'None' && t.isIdentifier(expr)) {
352
+ result = await this.resolveIdentifierKind(expr.name, fileId, visited)
353
+ }
354
+
355
+ if (result === 'None' && t.isTSAsExpression(expr)) {
356
+ result = await this.resolveExprKind(expr.expression, fileId, visited)
357
+ }
358
+ if (result === 'None' && t.isTSNonNullExpression(expr)) {
359
+ result = await this.resolveExprKind(expr.expression, fileId, visited)
360
+ }
361
+ if (result === 'None' && t.isParenthesizedExpression(expr)) {
362
+ result = await this.resolveExprKind(expr.expression, fileId, visited)
363
+ }
364
+
365
+ return result
366
+ }
367
+
368
+ private async resolveCalleeKind(
369
+ callee: t.Expression,
370
+ fileId: string,
371
+ visited = new Set<string>(),
372
+ ): Promise<Kind> {
373
+ if (t.isIdentifier(callee)) {
374
+ return this.resolveIdentifierKind(callee.name, fileId, visited)
375
+ }
376
+
377
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
378
+ const prop = callee.property.name
379
+
380
+ if (prop === 'handler') {
381
+ const base = await this.resolveExprKind(callee.object, fileId, visited)
382
+ if (base === 'Root' || base === 'Builder') {
383
+ return 'ServerFn'
384
+ }
385
+ return 'None'
386
+ }
387
+ // Check if the object is a namespace import
388
+ if (t.isIdentifier(callee.object)) {
389
+ const info = await this.getModuleInfo(fileId)
390
+ const binding = info.bindings.get(callee.object.name)
391
+ if (
392
+ binding &&
393
+ binding.type === 'import' &&
394
+ binding.importedName === '*'
395
+ ) {
396
+ // resolve the property from the target module
397
+ const targetModuleId = await this.options.resolveId(
398
+ binding.source,
399
+ fileId,
400
+ )
401
+ if (targetModuleId) {
402
+ const targetModule = await this.getModuleInfo(targetModuleId)
403
+ const exportEntry = targetModule.exports.get(callee.property.name)
404
+ if (exportEntry) {
405
+ const exportedBinding = targetModule.bindings.get(
406
+ exportEntry.name,
407
+ )
408
+ if (exportedBinding) {
409
+ return await this.resolveBindingKind(
410
+ exportedBinding,
411
+ targetModule.id,
412
+ visited,
413
+ )
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ return this.resolveExprKind(callee.object, fileId, visited)
420
+ }
421
+
422
+ // handle nested expressions
423
+ return this.resolveExprKind(callee, fileId, visited)
424
+ }
425
+
426
+ private async getModuleInfo(id: string) {
427
+ let cached = this.moduleCache.get(id)
428
+ if (cached) {
429
+ return cached
430
+ }
431
+
432
+ await this.options.loadModule(id)
433
+
434
+ cached = this.moduleCache.get(id)
435
+ if (!cached) {
436
+ throw new Error(`could not load module info for ${id}`)
437
+ }
438
+ return cached
439
+ }
440
+ }
441
+
442
+ function isHandlerCall(
443
+ node: t.Node | null | undefined,
444
+ ): undefined | t.CallExpression {
445
+ if (!t.isCallExpression(node)) return undefined
446
+
447
+ const callee = node.callee
448
+ if (
449
+ !t.isMemberExpression(callee) ||
450
+ !t.isIdentifier(callee.property, { name: 'handler' })
451
+ ) {
452
+ return undefined
453
+ }
454
+
455
+ return node
456
+ }
@@ -1,39 +1,32 @@
1
1
  import * as t from '@babel/types'
2
- import { codeFrameError, getRootCallExpression } from './utils'
2
+ import {
3
+ codeFrameError,
4
+ getRootCallExpression,
5
+ } from '../start-compiler-plugin/utils'
3
6
  import type * as babel from '@babel/core'
4
7
 
5
- import type { CompileOptions } from './compilers'
6
-
7
- export function handleCreateServerFnCallExpression(
8
+ export function handleCreateServerFn(
8
9
  path: babel.NodePath<t.CallExpression>,
9
- opts: CompileOptions,
10
+ opts: {
11
+ env: 'client' | 'server'
12
+ code: string
13
+ },
10
14
  ) {
11
15
  // Traverse the member expression and find the call expressions for
12
16
  // the validator, handler, and middleware methods. Check to make sure they
13
17
  // are children of the createServerFn call expression.
14
18
 
15
- const calledOptions = path.node.arguments[0]
16
- ? (path.get('arguments.0') as babel.NodePath<t.ObjectExpression>)
17
- : null
18
-
19
- const shouldValidateClient = !!calledOptions?.node.properties.find((prop) => {
20
- return (
21
- t.isObjectProperty(prop) &&
22
- t.isIdentifier(prop.key) &&
23
- prop.key.name === 'validateClient' &&
24
- t.isBooleanLiteral(prop.value) &&
25
- prop.value.value === true
26
- )
27
- })
28
-
29
- const callExpressionPaths = {
30
- middleware: null as babel.NodePath<t.CallExpression> | null,
31
- validator: null as babel.NodePath<t.CallExpression> | null,
32
- handler: null as babel.NodePath<t.CallExpression> | null,
19
+ const validMethods = ['middleware', 'validator', 'handler'] as const
20
+ type ValidMethods = (typeof validMethods)[number]
21
+ const callExpressionPaths: Record<
22
+ ValidMethods,
23
+ babel.NodePath<t.CallExpression> | null
24
+ > = {
25
+ middleware: null,
26
+ validator: null,
27
+ handler: null,
33
28
  }
34
29
 
35
- const validMethods = Object.keys(callExpressionPaths)
36
-
37
30
  const rootCallExpression = getRootCallExpression(path)
38
31
 
39
32
  // if (debug)
@@ -54,8 +47,7 @@ export function handleCreateServerFnCallExpression(
54
47
  rootCallExpression.traverse({
55
48
  MemberExpression(memberExpressionPath) {
56
49
  if (t.isIdentifier(memberExpressionPath.node.property)) {
57
- const name = memberExpressionPath.node.property
58
- .name as keyof typeof callExpressionPaths
50
+ const name = memberExpressionPath.node.property.name as ValidMethods
59
51
 
60
52
  if (
61
53
  validMethods.includes(name) &&
@@ -76,15 +68,13 @@ export function handleCreateServerFnCallExpression(
76
68
  )
77
69
  }
78
70
 
79
- // If we're on the client, and we're not validating the client, remove the validator call expression
80
- if (
81
- opts.env === 'client' &&
82
- !shouldValidateClient &&
83
- t.isMemberExpression(callExpressionPaths.validator.node.callee)
84
- ) {
85
- callExpressionPaths.validator.replaceWith(
86
- callExpressionPaths.validator.node.callee.object,
87
- )
71
+ // If we're on the client, remove the validator call expression
72
+ if (opts.env === 'client') {
73
+ if (t.isMemberExpression(callExpressionPaths.validator.node.callee)) {
74
+ callExpressionPaths.validator.replaceWith(
75
+ callExpressionPaths.validator.node.callee.object,
76
+ )
77
+ }
88
78
  }
89
79
  }
90
80
 
@@ -0,0 +1,138 @@
1
+ import { VITE_ENVIRONMENT_NAMES } from '../constants'
2
+ import { ServerFnCompiler } from './compiler'
3
+ import type { ViteEnvironmentNames } from '../constants'
4
+ import type { PluginOption } from 'vite'
5
+ import type { CompileStartFrameworkOptions } from '../start-compiler-plugin/compilers'
6
+
7
+ function cleanId(id: string): string {
8
+ return id.split('?')[0]!
9
+ }
10
+
11
+ export function createServerFnPlugin(
12
+ framework: CompileStartFrameworkOptions,
13
+ ): PluginOption {
14
+ const libName = `@tanstack/${framework}-start`
15
+ const rootExport = 'createServerFn'
16
+
17
+ const SERVER_FN_LOOKUP = 'server-fn-module-lookup'
18
+
19
+ const compilers: Partial<Record<ViteEnvironmentNames, ServerFnCompiler>> = {}
20
+ return [
21
+ {
22
+ name: 'tanstack-start-core:capture-server-fn-module-lookup',
23
+ // we only need this plugin in dev mode
24
+ apply: 'serve',
25
+ applyToEnvironment(env) {
26
+ return [
27
+ VITE_ENVIRONMENT_NAMES.client,
28
+ VITE_ENVIRONMENT_NAMES.server,
29
+ ].includes(env.name as ViteEnvironmentNames)
30
+ },
31
+ transform: {
32
+ filter: {
33
+ id: new RegExp(`${SERVER_FN_LOOKUP}$`),
34
+ },
35
+ handler(code, id) {
36
+ const compiler =
37
+ compilers[this.environment.name as ViteEnvironmentNames]
38
+ compiler?.ingestModule({ code, id: cleanId(id) })
39
+ },
40
+ },
41
+ },
42
+ {
43
+ name: 'tanstack-start-core::server-fn',
44
+ enforce: 'pre',
45
+
46
+ applyToEnvironment(env) {
47
+ return [
48
+ VITE_ENVIRONMENT_NAMES.client,
49
+ VITE_ENVIRONMENT_NAMES.server,
50
+ ].includes(env.name as ViteEnvironmentNames)
51
+ },
52
+ transform: {
53
+ filter: {
54
+ id: {
55
+ exclude: new RegExp(`${SERVER_FN_LOOKUP}$`),
56
+ },
57
+ code: {
58
+ // only scan files that mention `.handler(`
59
+ include: [/\.handler\(/],
60
+ },
61
+ },
62
+ async handler(code, id) {
63
+ let compiler =
64
+ compilers[this.environment.name as ViteEnvironmentNames]
65
+ if (!compiler) {
66
+ const env =
67
+ this.environment.name === VITE_ENVIRONMENT_NAMES.client
68
+ ? 'client'
69
+ : this.environment.name === VITE_ENVIRONMENT_NAMES.server
70
+ ? 'server'
71
+ : (() => {
72
+ throw new Error(
73
+ `Environment ${this.environment.name} not configured`,
74
+ )
75
+ })()
76
+
77
+ compiler = new ServerFnCompiler({
78
+ env,
79
+ libName,
80
+ rootExport,
81
+ loadModule: async (id: string) => {
82
+ if (this.environment.mode === 'build') {
83
+ const loaded = await this.load({ id })
84
+ if (!loaded.code) {
85
+ throw new Error(`could not load module ${id}`)
86
+ }
87
+ compiler!.ingestModule({ code: loaded.code, id })
88
+ } else if (this.environment.mode === 'dev') {
89
+ /**
90
+ * in dev, vite does not return code from `ctx.load()`
91
+ * so instead, we need to take a different approach
92
+ * we must force vite to load the module and run it through the vite plugin pipeline
93
+ * we can do this by using the `fetchModule` method
94
+ * the `captureServerFnModuleLookupPlugin` captures the module code via its transform hook and invokes analyzeModuleAST
95
+ */
96
+ await this.environment.fetchModule(
97
+ id + '?' + SERVER_FN_LOOKUP,
98
+ )
99
+ } else {
100
+ throw new Error(
101
+ `could not load module ${id}: unknown environment mode ${this.environment.mode}`,
102
+ )
103
+ }
104
+ },
105
+ resolveId: async (source: string, importer?: string) => {
106
+ const r = await this.resolve(source, importer)
107
+ return r ? cleanId(r.id) : null
108
+ },
109
+ })
110
+ compilers[this.environment.name as ViteEnvironmentNames] = compiler
111
+ }
112
+
113
+ id = cleanId(id)
114
+ const result = await compiler.compile({ id, code })
115
+ return result
116
+ },
117
+ },
118
+
119
+ hotUpdate(ctx) {
120
+ const compiler =
121
+ compilers[this.environment.name as ViteEnvironmentNames]
122
+
123
+ ctx.modules.forEach((m) => {
124
+ if (m.id) {
125
+ const deleted = compiler?.invalidateModule(m.id)
126
+ if (deleted) {
127
+ m.importers.forEach((importer) => {
128
+ if (importer.id) {
129
+ compiler?.invalidateModule(importer.id)
130
+ }
131
+ })
132
+ }
133
+ }
134
+ })
135
+ },
136
+ },
137
+ ]
138
+ }
package/src/plugin.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  getServerOutputDirectory,
19
19
  } from './output-directory'
20
20
  import { postServerBuild } from './post-server-build'
21
+ import { createServerFnPlugin } from './create-server-fn-plugin/plugin'
21
22
  import type { ViteEnvironmentNames } from './constants'
22
23
  import type { TanStackStartInputConfig } from './schema'
23
24
  import type { PluginOption } from 'vite'
@@ -289,11 +290,9 @@ export function TanStackStartVitePluginCore(
289
290
  }
290
291
  },
291
292
  },
293
+ createServerFnPlugin(corePluginOpts.framework),
292
294
  // N.B. TanStackStartCompilerPlugin must be before the TanStackServerFnPluginEnv
293
- startCompilerPlugin(corePluginOpts.framework, {
294
- client: { envName: VITE_ENVIRONMENT_NAMES.client },
295
- server: { envName: VITE_ENVIRONMENT_NAMES.server },
296
- }),
295
+ startCompilerPlugin(corePluginOpts.framework),
297
296
  TanStackServerFnPluginEnv({
298
297
  // This is the ID that will be available to look up and import
299
298
  // our server function manifest and resolve its module