@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.
- package/dist/esm/create-server-fn-plugin/compiler.d.ts +61 -0
- package/dist/esm/create-server-fn-plugin/compiler.js +336 -0
- package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -0
- package/dist/esm/create-server-fn-plugin/handleCreateServerFn.d.ts +6 -0
- package/dist/esm/{start-compiler-plugin/serverFn.js → create-server-fn-plugin/handleCreateServerFn.js} +11 -13
- package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js.map +1 -0
- package/dist/esm/create-server-fn-plugin/plugin.d.ts +3 -0
- package/dist/esm/create-server-fn-plugin/plugin.js +113 -0
- package/dist/esm/create-server-fn-plugin/plugin.js.map +1 -0
- package/dist/esm/plugin.js +3 -4
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/start-compiler-plugin/compilers.js +0 -6
- package/dist/esm/start-compiler-plugin/compilers.js.map +1 -1
- package/dist/esm/start-compiler-plugin/constants.d.ts +1 -1
- package/dist/esm/start-compiler-plugin/constants.js +0 -1
- package/dist/esm/start-compiler-plugin/constants.js.map +1 -1
- package/dist/esm/start-compiler-plugin/plugin.d.ts +1 -8
- package/dist/esm/start-compiler-plugin/plugin.js +6 -13
- package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
- package/package.json +7 -7
- package/src/create-server-fn-plugin/compiler.ts +456 -0
- package/src/{start-compiler-plugin/serverFn.ts → create-server-fn-plugin/handleCreateServerFn.ts} +26 -36
- package/src/create-server-fn-plugin/plugin.ts +138 -0
- package/src/plugin.ts +3 -4
- package/src/start-compiler-plugin/compilers.ts +0 -6
- package/src/start-compiler-plugin/constants.ts +0 -1
- package/src/start-compiler-plugin/plugin.ts +7 -22
- package/dist/esm/start-compiler-plugin/serverFn.d.ts +0 -4
- 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
|
+
}
|
package/src/{start-compiler-plugin/serverFn.ts → create-server-fn-plugin/handleCreateServerFn.ts}
RENAMED
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
import * as t from '@babel/types'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
codeFrameError,
|
|
4
|
+
getRootCallExpression,
|
|
5
|
+
} from '../start-compiler-plugin/utils'
|
|
3
6
|
import type * as babel from '@babel/core'
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export function handleCreateServerFnCallExpression(
|
|
8
|
+
export function handleCreateServerFn(
|
|
8
9
|
path: babel.NodePath<t.CallExpression>,
|
|
9
|
-
opts:
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|