@tanstack/start-plugin-core 1.142.4 → 1.142.6
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 +6 -0
- package/dist/esm/create-server-fn-plugin/compiler.js +146 -80
- package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.d.ts +8 -3
- package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.js +10 -33
- package/dist/esm/create-server-fn-plugin/handleCreateMiddleware.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/handleCreateServerFn.d.ts +8 -3
- package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js +28 -31
- package/dist/esm/create-server-fn-plugin/handleCreateServerFn.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/types.d.ts +31 -0
- package/package.json +6 -6
- package/src/create-server-fn-plugin/compiler.ts +215 -94
- package/src/create-server-fn-plugin/handleCreateMiddleware.ts +18 -44
- package/src/create-server-fn-plugin/handleCreateServerFn.ts +39 -60
- package/src/create-server-fn-plugin/types.ts +42 -0
|
@@ -1,53 +1,50 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
|
-
import {
|
|
3
|
-
function handleCreateServerFn(
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
inputValidator: null,
|
|
8
|
-
handler: null
|
|
9
|
-
};
|
|
10
|
-
const rootCallExpression = getRootCallExpression(path);
|
|
11
|
-
if (!rootCallExpression.parentPath.isVariableDeclarator()) {
|
|
2
|
+
import { codeFrameError } from "../start-compiler-plugin/utils.js";
|
|
3
|
+
function handleCreateServerFn(candidate, opts) {
|
|
4
|
+
const { path, methodChain } = candidate;
|
|
5
|
+
const { inputValidator, handler } = methodChain;
|
|
6
|
+
if (!path.parentPath.isVariableDeclarator()) {
|
|
12
7
|
throw new Error("createServerFn must be assigned to a variable!");
|
|
13
8
|
}
|
|
14
|
-
const variableDeclarator =
|
|
9
|
+
const variableDeclarator = path.parentPath.node;
|
|
10
|
+
if (!t.isIdentifier(variableDeclarator.id)) {
|
|
11
|
+
throw codeFrameError(
|
|
12
|
+
opts.code,
|
|
13
|
+
variableDeclarator.id.loc,
|
|
14
|
+
"createServerFn must be assigned to a simple identifier, not a destructuring pattern"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
15
17
|
const existingVariableName = variableDeclarator.id.name;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (t.isIdentifier(memberExpressionPath.node.property)) {
|
|
19
|
-
const name = memberExpressionPath.node.property.name;
|
|
20
|
-
if (validMethods.includes(name) && memberExpressionPath.parentPath.isCallExpression()) {
|
|
21
|
-
callExpressionPaths[name] = memberExpressionPath.parentPath;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
if (callExpressionPaths.inputValidator) {
|
|
27
|
-
const innerInputExpression = callExpressionPaths.inputValidator.node.arguments[0];
|
|
18
|
+
if (inputValidator) {
|
|
19
|
+
const innerInputExpression = inputValidator.callPath.node.arguments[0];
|
|
28
20
|
if (!innerInputExpression) {
|
|
29
21
|
throw new Error(
|
|
30
22
|
"createServerFn().inputValidator() must be called with a validator!"
|
|
31
23
|
);
|
|
32
24
|
}
|
|
33
25
|
if (opts.env === "client") {
|
|
34
|
-
if (t.isMemberExpression(
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
if (t.isMemberExpression(inputValidator.callPath.node.callee)) {
|
|
27
|
+
inputValidator.callPath.replaceWith(
|
|
28
|
+
inputValidator.callPath.node.callee.object
|
|
37
29
|
);
|
|
38
30
|
}
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
|
-
const handlerFnPath =
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
if (!callExpressionPaths.handler || !handlerFnPath.node) {
|
|
33
|
+
const handlerFnPath = handler?.firstArgPath;
|
|
34
|
+
if (!handler || !handlerFnPath?.node) {
|
|
45
35
|
throw codeFrameError(
|
|
46
36
|
opts.code,
|
|
47
37
|
path.node.callee.loc,
|
|
48
38
|
`createServerFn must be called with a "handler" property!`
|
|
49
39
|
);
|
|
50
40
|
}
|
|
41
|
+
if (!t.isExpression(handlerFnPath.node)) {
|
|
42
|
+
throw codeFrameError(
|
|
43
|
+
opts.code,
|
|
44
|
+
handlerFnPath.node.loc,
|
|
45
|
+
`handler() must be called with an expression, not a ${handlerFnPath.node.type}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
51
48
|
const handlerFn = handlerFnPath.node;
|
|
52
49
|
if (t.isIdentifier(handlerFn)) {
|
|
53
50
|
if (opts.env === "client") {
|
|
@@ -76,7 +73,7 @@ function handleCreateServerFn(path, opts) {
|
|
|
76
73
|
)
|
|
77
74
|
);
|
|
78
75
|
if (opts.env === "server" && opts.isProviderFile) {
|
|
79
|
-
|
|
76
|
+
handler.callPath.node.arguments.push(handlerFn);
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handleCreateServerFn.js","sources":["../../../src/create-server-fn-plugin/handleCreateServerFn.ts"],"sourcesContent":["import * as t from '@babel/types'\nimport {
|
|
1
|
+
{"version":3,"file":"handleCreateServerFn.js","sources":["../../../src/create-server-fn-plugin/handleCreateServerFn.ts"],"sourcesContent":["import * as t from '@babel/types'\nimport { codeFrameError } from '../start-compiler-plugin/utils'\nimport type { RewriteCandidate } from './types'\n\n/**\n * Handles createServerFn transformations.\n *\n * @param candidate - The rewrite candidate containing path and method chain\n * @param opts - Options including the environment, code, directive, and provider file flag\n */\nexport function handleCreateServerFn(\n candidate: RewriteCandidate,\n opts: {\n env: 'client' | 'server'\n code: string\n directive: string\n /**\n * Whether this file is a provider file (extracted server function file).\n * Only provider files should have the handler implementation as a second argument.\n */\n isProviderFile: boolean\n },\n) {\n const { path, methodChain } = candidate\n const { inputValidator, handler } = methodChain\n\n // Check if the call is assigned to a variable\n if (!path.parentPath.isVariableDeclarator()) {\n throw new Error('createServerFn must be assigned to a variable!')\n }\n\n // Get the identifier name of the variable\n const variableDeclarator = path.parentPath.node\n if (!t.isIdentifier(variableDeclarator.id)) {\n throw codeFrameError(\n opts.code,\n variableDeclarator.id.loc!,\n 'createServerFn must be assigned to a simple identifier, not a destructuring pattern',\n )\n }\n const existingVariableName = variableDeclarator.id.name\n\n if (inputValidator) {\n const innerInputExpression = inputValidator.callPath.node.arguments[0]\n\n if (!innerInputExpression) {\n throw new Error(\n 'createServerFn().inputValidator() must be called with a validator!',\n )\n }\n\n // If we're on the client, remove the validator call expression\n if (opts.env === 'client') {\n if (t.isMemberExpression(inputValidator.callPath.node.callee)) {\n inputValidator.callPath.replaceWith(\n inputValidator.callPath.node.callee.object,\n )\n }\n }\n }\n\n // First, we need to move the handler function to a nested function call\n // that is applied to the arguments passed to the server function.\n\n const handlerFnPath = handler?.firstArgPath\n\n if (!handler || !handlerFnPath?.node) {\n throw codeFrameError(\n opts.code,\n path.node.callee.loc!,\n `createServerFn must be called with a \"handler\" property!`,\n )\n }\n\n // Validate the handler argument is an expression (not a SpreadElement, etc.)\n if (!t.isExpression(handlerFnPath.node)) {\n throw codeFrameError(\n opts.code,\n handlerFnPath.node.loc!,\n `handler() must be called with an expression, not a ${handlerFnPath.node.type}`,\n )\n }\n\n const handlerFn = handlerFnPath.node\n\n // So, the way we do this is we give the handler function a way\n // to access the serverFn ctx on the server via function scope.\n // The 'use server' extracted function will be called with the\n // payload from the client, then use the scoped serverFn ctx\n // to execute the handler function.\n // This way, we can do things like data and middleware validation\n // in the __execute function without having to AST transform the\n // handler function too much itself.\n\n // .handler((optsOut, ctx) => {\n // return ((optsIn) => {\n // 'use server'\n // ctx.__execute(handlerFn, optsIn)\n // })(optsOut)\n // })\n\n // If the handler function is an identifier and we're on the client, we need to\n // remove the bound function from the file.\n // If we're on the server, you can leave it, since it will get referenced\n // as a second argument.\n\n if (t.isIdentifier(handlerFn)) {\n if (opts.env === 'client') {\n // Find the binding for the handler function\n const binding = handlerFnPath.scope.getBinding(handlerFn.name)\n // Remove it\n if (binding) {\n binding.path.remove()\n }\n }\n // If the env is server, just leave it alone\n }\n\n handlerFnPath.replaceWith(\n t.arrowFunctionExpression(\n [t.identifier('opts'), t.identifier('signal')],\n t.blockStatement(\n // Everything in here is server-only, since the client\n // will strip out anything in the 'use server' directive.\n [\n t.returnStatement(\n t.callExpression(\n t.identifier(`${existingVariableName}.__executeServer`),\n [t.identifier('opts'), t.identifier('signal')],\n ),\n ),\n ],\n [t.directive(t.directiveLiteral(opts.directive))],\n ),\n ),\n )\n\n // Add the serverFn as a second argument on the server side,\n // but ONLY for provider files (extracted server function files).\n // Caller files must NOT have the second argument because the implementation is already available in the extracted chunk\n // and including it would duplicate code\n if (opts.env === 'server' && opts.isProviderFile) {\n handler.callPath.node.arguments.push(handlerFn)\n }\n}\n"],"names":[],"mappings":";;AAUO,SAAS,qBACd,WACA,MAUA;AACA,QAAM,EAAE,MAAM,YAAA,IAAgB;AAC9B,QAAM,EAAE,gBAAgB,QAAA,IAAY;AAGpC,MAAI,CAAC,KAAK,WAAW,wBAAwB;AAC3C,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AAGA,QAAM,qBAAqB,KAAK,WAAW;AAC3C,MAAI,CAAC,EAAE,aAAa,mBAAmB,EAAE,GAAG;AAC1C,UAAM;AAAA,MACJ,KAAK;AAAA,MACL,mBAAmB,GAAG;AAAA,MACtB;AAAA,IAAA;AAAA,EAEJ;AACA,QAAM,uBAAuB,mBAAmB,GAAG;AAEnD,MAAI,gBAAgB;AAClB,UAAM,uBAAuB,eAAe,SAAS,KAAK,UAAU,CAAC;AAErE,QAAI,CAAC,sBAAsB;AACzB,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAGA,QAAI,KAAK,QAAQ,UAAU;AACzB,UAAI,EAAE,mBAAmB,eAAe,SAAS,KAAK,MAAM,GAAG;AAC7D,uBAAe,SAAS;AAAA,UACtB,eAAe,SAAS,KAAK,OAAO;AAAA,QAAA;AAAA,MAExC;AAAA,IACF;AAAA,EACF;AAKA,QAAM,gBAAgB,SAAS;AAE/B,MAAI,CAAC,WAAW,CAAC,eAAe,MAAM;AACpC,UAAM;AAAA,MACJ,KAAK;AAAA,MACL,KAAK,KAAK,OAAO;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,CAAC,EAAE,aAAa,cAAc,IAAI,GAAG;AACvC,UAAM;AAAA,MACJ,KAAK;AAAA,MACL,cAAc,KAAK;AAAA,MACnB,sDAAsD,cAAc,KAAK,IAAI;AAAA,IAAA;AAAA,EAEjF;AAEA,QAAM,YAAY,cAAc;AAuBhC,MAAI,EAAE,aAAa,SAAS,GAAG;AAC7B,QAAI,KAAK,QAAQ,UAAU;AAEzB,YAAM,UAAU,cAAc,MAAM,WAAW,UAAU,IAAI;AAE7D,UAAI,SAAS;AACX,gBAAQ,KAAK,OAAA;AAAA,MACf;AAAA,IACF;AAAA,EAEF;AAEA,gBAAc;AAAA,IACZ,EAAE;AAAA,MACA,CAAC,EAAE,WAAW,MAAM,GAAG,EAAE,WAAW,QAAQ,CAAC;AAAA,MAC7C,EAAE;AAAA;AAAA;AAAA,QAGA;AAAA,UACE,EAAE;AAAA,YACA,EAAE;AAAA,cACA,EAAE,WAAW,GAAG,oBAAoB,kBAAkB;AAAA,cACtD,CAAC,EAAE,WAAW,MAAM,GAAG,EAAE,WAAW,QAAQ,CAAC;AAAA,YAAA;AAAA,UAC/C;AAAA,QACF;AAAA,QAEF,CAAC,EAAE,UAAU,EAAE,iBAAiB,KAAK,SAAS,CAAC,CAAC;AAAA,MAAA;AAAA,IAClD;AAAA,EACF;AAOF,MAAI,KAAK,QAAQ,YAAY,KAAK,gBAAgB;AAChD,YAAQ,SAAS,KAAK,UAAU,KAAK,SAAS;AAAA,EAChD;AACF;"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as babel from '@babel/core';
|
|
2
|
+
import type * as t from '@babel/types';
|
|
3
|
+
/**
|
|
4
|
+
* Info about a method call in the chain, including the call expression path
|
|
5
|
+
* and the path to its first argument (if any).
|
|
6
|
+
*/
|
|
7
|
+
export interface MethodCallInfo {
|
|
8
|
+
callPath: babel.NodePath<t.CallExpression>;
|
|
9
|
+
/** Path to the first argument, or null if no arguments */
|
|
10
|
+
firstArgPath: babel.NodePath | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pre-collected method chain paths for a root call expression.
|
|
14
|
+
* This avoids needing to traverse the AST again in handlers.
|
|
15
|
+
*/
|
|
16
|
+
export interface MethodChainPaths {
|
|
17
|
+
middleware: MethodCallInfo | null;
|
|
18
|
+
inputValidator: MethodCallInfo | null;
|
|
19
|
+
handler: MethodCallInfo | null;
|
|
20
|
+
server: MethodCallInfo | null;
|
|
21
|
+
client: MethodCallInfo | null;
|
|
22
|
+
}
|
|
23
|
+
export type MethodChainKey = keyof MethodChainPaths;
|
|
24
|
+
export declare const METHOD_CHAIN_KEYS: ReadonlyArray<MethodChainKey>;
|
|
25
|
+
/**
|
|
26
|
+
* Information about a candidate that needs to be rewritten.
|
|
27
|
+
*/
|
|
28
|
+
export interface RewriteCandidate {
|
|
29
|
+
path: babel.NodePath<t.CallExpression>;
|
|
30
|
+
methodChain: MethodChainPaths;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/start-plugin-core",
|
|
3
|
-
"version": "1.142.
|
|
3
|
+
"version": "1.142.6",
|
|
4
4
|
"description": "Modern and scalable routing for React applications",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"vitefu": "^1.1.1",
|
|
60
60
|
"xmlbuilder2": "^4.0.0",
|
|
61
61
|
"zod": "^3.24.2",
|
|
62
|
-
"@tanstack/router-core": "1.
|
|
63
|
-
"@tanstack/router-
|
|
64
|
-
"@tanstack/router-
|
|
62
|
+
"@tanstack/router-core": "1.142.6",
|
|
63
|
+
"@tanstack/router-plugin": "1.142.6",
|
|
64
|
+
"@tanstack/router-generator": "1.142.6",
|
|
65
65
|
"@tanstack/router-utils": "1.141.0",
|
|
66
66
|
"@tanstack/server-functions-plugin": "1.142.1",
|
|
67
|
-
"@tanstack/start-client-core": "1.142.
|
|
68
|
-
"@tanstack/start-server-core": "1.142.
|
|
67
|
+
"@tanstack/start-client-core": "1.142.6",
|
|
68
|
+
"@tanstack/start-server-core": "1.142.6"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/babel__code-frame": "^7.0.6",
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from 'babel-dead-code-elimination'
|
|
9
9
|
import { handleCreateServerFn } from './handleCreateServerFn'
|
|
10
10
|
import { handleCreateMiddleware } from './handleCreateMiddleware'
|
|
11
|
+
import type { MethodChainPaths, RewriteCandidate } from './types'
|
|
11
12
|
|
|
12
13
|
type Binding =
|
|
13
14
|
| {
|
|
@@ -41,6 +42,16 @@ const LookupSetup: Record<
|
|
|
41
42
|
},
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// Pre-computed map: identifier name -> LookupKind for fast candidate detection
|
|
46
|
+
const IdentifierToKind = new Map<string, LookupKind>()
|
|
47
|
+
for (const [kind, setup] of Object.entries(LookupSetup) as Array<
|
|
48
|
+
[LookupKind, { candidateCallIdentifier: Set<string> }]
|
|
49
|
+
>) {
|
|
50
|
+
for (const id of setup.candidateCallIdentifier) {
|
|
51
|
+
IdentifierToKind.set(id, kind)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
export type LookupConfig = {
|
|
45
56
|
libName: string
|
|
46
57
|
rootExport: string
|
|
@@ -59,6 +70,10 @@ export class ServerFnCompiler {
|
|
|
59
70
|
private moduleCache = new Map<string, ModuleInfo>()
|
|
60
71
|
private initialized = false
|
|
61
72
|
private validLookupKinds: Set<LookupKind>
|
|
73
|
+
// Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
|
|
74
|
+
// Maps: libName → (exportName → Kind)
|
|
75
|
+
// This allows O(1) resolution for the common case without async resolveId calls
|
|
76
|
+
private knownRootImports = new Map<string, Map<string, Kind>>()
|
|
62
77
|
constructor(
|
|
63
78
|
private options: {
|
|
64
79
|
env: 'client' | 'server'
|
|
@@ -108,6 +123,14 @@ export class ServerFnCompiler {
|
|
|
108
123
|
resolvedKind: `Root` satisfies Kind,
|
|
109
124
|
})
|
|
110
125
|
this.moduleCache.set(libId, rootModule)
|
|
126
|
+
|
|
127
|
+
// Also populate the fast lookup map for direct imports
|
|
128
|
+
let libExports = this.knownRootImports.get(config.libName)
|
|
129
|
+
if (!libExports) {
|
|
130
|
+
libExports = new Map()
|
|
131
|
+
this.knownRootImports.set(config.libName, libExports)
|
|
132
|
+
}
|
|
133
|
+
libExports.set(config.rootExport, 'Root')
|
|
111
134
|
}),
|
|
112
135
|
)
|
|
113
136
|
|
|
@@ -247,36 +270,98 @@ export class ServerFnCompiler {
|
|
|
247
270
|
}
|
|
248
271
|
|
|
249
272
|
// let's find out which of the candidates are actually server functions
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
// Resolve all candidates in parallel for better performance
|
|
274
|
+
const resolvedCandidates = await Promise.all(
|
|
275
|
+
candidates.map(async (candidate) => ({
|
|
276
|
+
candidate,
|
|
277
|
+
kind: await this.resolveExprKind(candidate, id),
|
|
278
|
+
})),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
// Map from candidate/root node -> kind
|
|
282
|
+
// Note: For top-level variable declarations, candidate === root (the outermost CallExpression)
|
|
283
|
+
const toRewriteMap = new Map<t.CallExpression, LookupKind>()
|
|
284
|
+
for (const { candidate, kind } of resolvedCandidates) {
|
|
256
285
|
if (this.validLookupKinds.has(kind as LookupKind)) {
|
|
257
|
-
|
|
286
|
+
toRewriteMap.set(candidate, kind as LookupKind)
|
|
258
287
|
}
|
|
259
288
|
}
|
|
260
|
-
if (
|
|
289
|
+
if (toRewriteMap.size === 0) {
|
|
261
290
|
return null
|
|
262
291
|
}
|
|
263
292
|
|
|
293
|
+
// Single-pass traversal to find NodePaths and collect method chains
|
|
264
294
|
const pathsToRewrite: Array<{
|
|
265
|
-
|
|
295
|
+
path: babel.NodePath<t.CallExpression>
|
|
266
296
|
kind: LookupKind
|
|
297
|
+
methodChain: MethodChainPaths
|
|
267
298
|
}> = []
|
|
299
|
+
|
|
300
|
+
// First, collect all CallExpression paths in the AST for O(1) lookup
|
|
301
|
+
const callExprPaths = new Map<
|
|
302
|
+
t.CallExpression,
|
|
303
|
+
babel.NodePath<t.CallExpression>
|
|
304
|
+
>()
|
|
305
|
+
|
|
268
306
|
babel.traverse(ast, {
|
|
269
307
|
CallExpression(path) {
|
|
270
|
-
|
|
271
|
-
if (found !== -1) {
|
|
272
|
-
pathsToRewrite.push({ nodePath: path, kind: toRewrite[found]!.kind })
|
|
273
|
-
// delete from toRewrite
|
|
274
|
-
toRewrite.splice(found, 1)
|
|
275
|
-
}
|
|
308
|
+
callExprPaths.set(path.node, path)
|
|
276
309
|
},
|
|
277
310
|
})
|
|
278
311
|
|
|
279
|
-
|
|
312
|
+
// Now process candidates - we can look up any CallExpression path in O(1)
|
|
313
|
+
for (const [node, kind] of toRewriteMap) {
|
|
314
|
+
const path = callExprPaths.get(node)
|
|
315
|
+
if (!path) {
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Collect method chain paths by walking DOWN from root through the chain
|
|
320
|
+
const methodChain: MethodChainPaths = {
|
|
321
|
+
middleware: null,
|
|
322
|
+
inputValidator: null,
|
|
323
|
+
handler: null,
|
|
324
|
+
server: null,
|
|
325
|
+
client: null,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Walk down the call chain using nodes, look up paths from map
|
|
329
|
+
let currentNode: t.CallExpression = node
|
|
330
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
331
|
+
while (true) {
|
|
332
|
+
const callee = currentNode.callee
|
|
333
|
+
if (!t.isMemberExpression(callee)) {
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Record method chain path if it's a known method
|
|
338
|
+
if (t.isIdentifier(callee.property)) {
|
|
339
|
+
const name = callee.property.name as keyof MethodChainPaths
|
|
340
|
+
if (name in methodChain) {
|
|
341
|
+
const currentPath = callExprPaths.get(currentNode)!
|
|
342
|
+
// Get first argument path
|
|
343
|
+
const args = currentPath.get('arguments')
|
|
344
|
+
const firstArgPath =
|
|
345
|
+
Array.isArray(args) && args.length > 0 ? (args[0] ?? null) : null
|
|
346
|
+
methodChain[name] = {
|
|
347
|
+
callPath: currentPath,
|
|
348
|
+
firstArgPath,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Move to the inner call (the object of the member expression)
|
|
354
|
+
if (!t.isCallExpression(callee.object)) {
|
|
355
|
+
break
|
|
356
|
+
}
|
|
357
|
+
currentNode = callee.object
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pathsToRewrite.push({ path, kind, methodChain })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Verify we found all candidates (pathsToRewrite should have same size as toRewriteMap had)
|
|
364
|
+
if (pathsToRewrite.length !== toRewriteMap.size) {
|
|
280
365
|
throw new Error(
|
|
281
366
|
`Internal error: could not find all paths to rewrite. please file an issue`,
|
|
282
367
|
)
|
|
@@ -284,18 +369,21 @@ export class ServerFnCompiler {
|
|
|
284
369
|
|
|
285
370
|
const refIdents = findReferencedIdentifiers(ast)
|
|
286
371
|
|
|
287
|
-
pathsToRewrite
|
|
288
|
-
|
|
289
|
-
|
|
372
|
+
for (const { path, kind, methodChain } of pathsToRewrite) {
|
|
373
|
+
const candidate: RewriteCandidate = { path, methodChain }
|
|
374
|
+
if (kind === 'ServerFn') {
|
|
375
|
+
handleCreateServerFn(candidate, {
|
|
290
376
|
env: this.options.env,
|
|
291
377
|
code,
|
|
292
378
|
directive: this.options.directive,
|
|
293
379
|
isProviderFile,
|
|
294
380
|
})
|
|
295
381
|
} else {
|
|
296
|
-
handleCreateMiddleware(
|
|
382
|
+
handleCreateMiddleware(candidate, {
|
|
383
|
+
env: this.options.env,
|
|
384
|
+
})
|
|
297
385
|
}
|
|
298
|
-
}
|
|
386
|
+
}
|
|
299
387
|
|
|
300
388
|
deadCodeElimination(ast, refIdents)
|
|
301
389
|
|
|
@@ -312,12 +400,12 @@ export class ServerFnCompiler {
|
|
|
312
400
|
|
|
313
401
|
for (const binding of bindings.values()) {
|
|
314
402
|
if (binding.type === 'var') {
|
|
315
|
-
const
|
|
403
|
+
const candidate = isCandidateCallExpression(
|
|
316
404
|
binding.init,
|
|
317
405
|
this.validLookupKinds,
|
|
318
406
|
)
|
|
319
|
-
if (
|
|
320
|
-
candidates.push(
|
|
407
|
+
if (candidate) {
|
|
408
|
+
candidates.push(candidate)
|
|
321
409
|
}
|
|
322
410
|
}
|
|
323
411
|
}
|
|
@@ -352,6 +440,61 @@ export class ServerFnCompiler {
|
|
|
352
440
|
return resolvedKind
|
|
353
441
|
}
|
|
354
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Recursively find an export in a module, following `export * from` chains.
|
|
445
|
+
* Returns the module info and binding if found, or undefined if not found.
|
|
446
|
+
*/
|
|
447
|
+
private async findExportInModule(
|
|
448
|
+
moduleInfo: ModuleInfo,
|
|
449
|
+
exportName: string,
|
|
450
|
+
visitedModules = new Set<string>(),
|
|
451
|
+
): Promise<{ moduleInfo: ModuleInfo; binding: Binding } | undefined> {
|
|
452
|
+
// Prevent infinite loops in circular re-exports
|
|
453
|
+
if (visitedModules.has(moduleInfo.id)) {
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
visitedModules.add(moduleInfo.id)
|
|
457
|
+
|
|
458
|
+
// First check direct exports
|
|
459
|
+
const directExport = moduleInfo.exports.get(exportName)
|
|
460
|
+
if (directExport) {
|
|
461
|
+
const binding = moduleInfo.bindings.get(directExport.name)
|
|
462
|
+
if (binding) {
|
|
463
|
+
return { moduleInfo, binding }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// If not found, recursively check re-export-all sources in parallel
|
|
468
|
+
// Valid code won't have duplicate exports across chains, so first match wins
|
|
469
|
+
if (moduleInfo.reExportAllSources.length > 0) {
|
|
470
|
+
const results = await Promise.all(
|
|
471
|
+
moduleInfo.reExportAllSources.map(async (reExportSource) => {
|
|
472
|
+
const reExportTarget = await this.options.resolveId(
|
|
473
|
+
reExportSource,
|
|
474
|
+
moduleInfo.id,
|
|
475
|
+
)
|
|
476
|
+
if (reExportTarget) {
|
|
477
|
+
const reExportModule = await this.getModuleInfo(reExportTarget)
|
|
478
|
+
return this.findExportInModule(
|
|
479
|
+
reExportModule,
|
|
480
|
+
exportName,
|
|
481
|
+
visitedModules,
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
return undefined
|
|
485
|
+
}),
|
|
486
|
+
)
|
|
487
|
+
// Return the first valid result
|
|
488
|
+
for (const result of results) {
|
|
489
|
+
if (result) {
|
|
490
|
+
return result
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return undefined
|
|
496
|
+
}
|
|
497
|
+
|
|
355
498
|
private async resolveBindingKind(
|
|
356
499
|
binding: Binding,
|
|
357
500
|
fileId: string,
|
|
@@ -361,6 +504,19 @@ export class ServerFnCompiler {
|
|
|
361
504
|
return binding.resolvedKind
|
|
362
505
|
}
|
|
363
506
|
if (binding.type === 'import') {
|
|
507
|
+
// Fast path: check if this is a direct import from a known library
|
|
508
|
+
// (e.g., import { createServerFn } from '@tanstack/react-start')
|
|
509
|
+
// This avoids async resolveId calls for the common case
|
|
510
|
+
const knownExports = this.knownRootImports.get(binding.source)
|
|
511
|
+
if (knownExports) {
|
|
512
|
+
const kind = knownExports.get(binding.importedName)
|
|
513
|
+
if (kind) {
|
|
514
|
+
binding.resolvedKind = kind
|
|
515
|
+
return kind
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Slow path: resolve through the module graph
|
|
364
520
|
const target = await this.options.resolveId(binding.source, fileId)
|
|
365
521
|
if (!target) {
|
|
366
522
|
return 'None'
|
|
@@ -368,60 +524,28 @@ export class ServerFnCompiler {
|
|
|
368
524
|
|
|
369
525
|
const importedModule = await this.getModuleInfo(target)
|
|
370
526
|
|
|
371
|
-
//
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
for (const reExportSource of importedModule.reExportAllSources) {
|
|
377
|
-
const reExportTarget = await this.options.resolveId(
|
|
378
|
-
reExportSource,
|
|
379
|
-
importedModule.id,
|
|
380
|
-
)
|
|
381
|
-
if (reExportTarget) {
|
|
382
|
-
const reExportModule = await this.getModuleInfo(reExportTarget)
|
|
383
|
-
const reExportEntry = reExportModule.exports.get(
|
|
384
|
-
binding.importedName,
|
|
385
|
-
)
|
|
386
|
-
if (reExportEntry) {
|
|
387
|
-
// Found the export in a re-exported module, resolve from there
|
|
388
|
-
const reExportBinding = reExportModule.bindings.get(
|
|
389
|
-
reExportEntry.name,
|
|
390
|
-
)
|
|
391
|
-
if (reExportBinding) {
|
|
392
|
-
if (reExportBinding.resolvedKind) {
|
|
393
|
-
return reExportBinding.resolvedKind
|
|
394
|
-
}
|
|
395
|
-
const resolvedKind = await this.resolveBindingKind(
|
|
396
|
-
reExportBinding,
|
|
397
|
-
reExportModule.id,
|
|
398
|
-
visited,
|
|
399
|
-
)
|
|
400
|
-
reExportBinding.resolvedKind = resolvedKind
|
|
401
|
-
return resolvedKind
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
527
|
+
// Find the export, recursively searching through export * from chains
|
|
528
|
+
const found = await this.findExportInModule(
|
|
529
|
+
importedModule,
|
|
530
|
+
binding.importedName,
|
|
531
|
+
)
|
|
407
532
|
|
|
408
|
-
if (!
|
|
533
|
+
if (!found) {
|
|
409
534
|
return 'None'
|
|
410
535
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
return importedBinding.resolvedKind
|
|
536
|
+
|
|
537
|
+
const { moduleInfo: foundModule, binding: foundBinding } = found
|
|
538
|
+
|
|
539
|
+
if (foundBinding.resolvedKind) {
|
|
540
|
+
return foundBinding.resolvedKind
|
|
417
541
|
}
|
|
418
542
|
|
|
419
543
|
const resolvedKind = await this.resolveBindingKind(
|
|
420
|
-
|
|
421
|
-
|
|
544
|
+
foundBinding,
|
|
545
|
+
foundModule.id,
|
|
422
546
|
visited,
|
|
423
547
|
)
|
|
424
|
-
|
|
548
|
+
foundBinding.resolvedKind = resolvedKind
|
|
425
549
|
return resolvedKind
|
|
426
550
|
}
|
|
427
551
|
|
|
@@ -443,6 +567,15 @@ export class ServerFnCompiler {
|
|
|
443
567
|
return 'None'
|
|
444
568
|
}
|
|
445
569
|
|
|
570
|
+
// Unwrap common TypeScript/parenthesized wrappers first for efficiency
|
|
571
|
+
while (
|
|
572
|
+
t.isTSAsExpression(expr) ||
|
|
573
|
+
t.isTSNonNullExpression(expr) ||
|
|
574
|
+
t.isParenthesizedExpression(expr)
|
|
575
|
+
) {
|
|
576
|
+
expr = expr.expression
|
|
577
|
+
}
|
|
578
|
+
|
|
446
579
|
let result: Kind = 'None'
|
|
447
580
|
|
|
448
581
|
if (t.isCallExpression(expr)) {
|
|
@@ -454,15 +587,12 @@ export class ServerFnCompiler {
|
|
|
454
587
|
fileId,
|
|
455
588
|
visited,
|
|
456
589
|
)
|
|
457
|
-
if (calleeKind
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return kind
|
|
464
|
-
}
|
|
465
|
-
}
|
|
590
|
+
if (calleeKind === 'Root' || calleeKind === 'Builder') {
|
|
591
|
+
return 'Builder'
|
|
592
|
+
}
|
|
593
|
+
// Use direct Set.has() instead of iterating
|
|
594
|
+
if (this.validLookupKinds.has(calleeKind as LookupKind)) {
|
|
595
|
+
return calleeKind
|
|
466
596
|
}
|
|
467
597
|
} else if (t.isMemberExpression(expr) && t.isIdentifier(expr.property)) {
|
|
468
598
|
result = await this.resolveCalleeKind(expr.object, fileId, visited)
|
|
@@ -472,16 +602,6 @@ export class ServerFnCompiler {
|
|
|
472
602
|
result = await this.resolveIdentifierKind(expr.name, fileId, visited)
|
|
473
603
|
}
|
|
474
604
|
|
|
475
|
-
if (result === 'None' && t.isTSAsExpression(expr)) {
|
|
476
|
-
result = await this.resolveExprKind(expr.expression, fileId, visited)
|
|
477
|
-
}
|
|
478
|
-
if (result === 'None' && t.isTSNonNullExpression(expr)) {
|
|
479
|
-
result = await this.resolveExprKind(expr.expression, fileId, visited)
|
|
480
|
-
}
|
|
481
|
-
if (result === 'None' && t.isParenthesizedExpression(expr)) {
|
|
482
|
-
result = await this.resolveExprKind(expr.expression, fileId, visited)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
605
|
return result
|
|
486
606
|
}
|
|
487
607
|
|
|
@@ -576,17 +696,18 @@ export class ServerFnCompiler {
|
|
|
576
696
|
function isCandidateCallExpression(
|
|
577
697
|
node: t.Node | null | undefined,
|
|
578
698
|
lookupKinds: Set<LookupKind>,
|
|
579
|
-
):
|
|
699
|
+
): t.CallExpression | undefined {
|
|
580
700
|
if (!t.isCallExpression(node)) return undefined
|
|
581
701
|
|
|
582
702
|
const callee = node.callee
|
|
583
703
|
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) {
|
|
584
704
|
return undefined
|
|
585
705
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
706
|
+
|
|
707
|
+
// Use pre-computed map for O(1) lookup instead of iterating over lookupKinds
|
|
708
|
+
const kind = IdentifierToKind.get(callee.property.name)
|
|
709
|
+
if (kind && lookupKinds.has(kind)) {
|
|
710
|
+
return node
|
|
590
711
|
}
|
|
591
712
|
|
|
592
713
|
return undefined
|