@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.
@@ -1,53 +1,50 @@
1
1
  import * as t from "@babel/types";
2
- import { getRootCallExpression, codeFrameError } from "../start-compiler-plugin/utils.js";
3
- function handleCreateServerFn(path, opts) {
4
- const validMethods = ["middleware", "inputValidator", "handler"];
5
- const callExpressionPaths = {
6
- middleware: null,
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 = rootCallExpression.parentPath.node;
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
- rootCallExpression.traverse({
17
- MemberExpression(memberExpressionPath) {
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(callExpressionPaths.inputValidator.node.callee)) {
35
- callExpressionPaths.inputValidator.replaceWith(
36
- callExpressionPaths.inputValidator.node.callee.object
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 = callExpressionPaths.handler?.get(
42
- "arguments.0"
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
- callExpressionPaths.handler.node.arguments.push(handlerFn);
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 {\n codeFrameError,\n getRootCallExpression,\n} from '../start-compiler-plugin/utils'\nimport type * as babel from '@babel/core'\n\nexport function handleCreateServerFn(\n path: babel.NodePath<t.CallExpression>,\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 // Traverse the member expression and find the call expressions for\n // the validator, handler, and middleware methods. Check to make sure they\n // are children of the createServerFn call expression.\n\n const validMethods = ['middleware', 'inputValidator', 'handler'] as const\n type ValidMethods = (typeof validMethods)[number]\n const callExpressionPaths: Record<\n ValidMethods,\n babel.NodePath<t.CallExpression> | null\n > = {\n middleware: null,\n inputValidator: null,\n handler: null,\n }\n\n const rootCallExpression = getRootCallExpression(path)\n\n // if (debug)\n // console.info(\n // 'Handling createServerFn call expression:',\n // rootCallExpression.toString(),\n // )\n\n // Check if the call is assigned to a variable\n if (!rootCallExpression.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 = rootCallExpression.parentPath.node\n const existingVariableName = (variableDeclarator.id as t.Identifier).name\n\n rootCallExpression.traverse({\n MemberExpression(memberExpressionPath) {\n if (t.isIdentifier(memberExpressionPath.node.property)) {\n const name = memberExpressionPath.node.property.name as ValidMethods\n\n if (\n validMethods.includes(name) &&\n memberExpressionPath.parentPath.isCallExpression()\n ) {\n callExpressionPaths[name] = memberExpressionPath.parentPath\n }\n }\n },\n })\n\n if (callExpressionPaths.inputValidator) {\n const innerInputExpression =\n callExpressionPaths.inputValidator.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 (\n t.isMemberExpression(callExpressionPaths.inputValidator.node.callee)\n ) {\n callExpressionPaths.inputValidator.replaceWith(\n callExpressionPaths.inputValidator.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 = callExpressionPaths.handler?.get(\n 'arguments.0',\n ) as babel.NodePath<any>\n\n if (!callExpressionPaths.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 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 callExpressionPaths.handler.node.arguments.push(handlerFn)\n }\n}\n"],"names":[],"mappings":";;AAOO,SAAS,qBACd,MACA,MAUA;AAKA,QAAM,eAAe,CAAC,cAAc,kBAAkB,SAAS;AAE/D,QAAM,sBAGF;AAAA,IACF,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,SAAS;AAAA,EAAA;AAGX,QAAM,qBAAqB,sBAAsB,IAAI;AASrD,MAAI,CAAC,mBAAmB,WAAW,wBAAwB;AACzD,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AAGA,QAAM,qBAAqB,mBAAmB,WAAW;AACzD,QAAM,uBAAwB,mBAAmB,GAAoB;AAErE,qBAAmB,SAAS;AAAA,IAC1B,iBAAiB,sBAAsB;AACrC,UAAI,EAAE,aAAa,qBAAqB,KAAK,QAAQ,GAAG;AACtD,cAAM,OAAO,qBAAqB,KAAK,SAAS;AAEhD,YACE,aAAa,SAAS,IAAI,KAC1B,qBAAqB,WAAW,oBAChC;AACA,8BAAoB,IAAI,IAAI,qBAAqB;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,EAAA,CACD;AAED,MAAI,oBAAoB,gBAAgB;AACtC,UAAM,uBACJ,oBAAoB,eAAe,KAAK,UAAU,CAAC;AAErD,QAAI,CAAC,sBAAsB;AACzB,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAGA,QAAI,KAAK,QAAQ,UAAU;AACzB,UACE,EAAE,mBAAmB,oBAAoB,eAAe,KAAK,MAAM,GACnE;AACA,4BAAoB,eAAe;AAAA,UACjC,oBAAoB,eAAe,KAAK,OAAO;AAAA,QAAA;AAAA,MAEnD;AAAA,IACF;AAAA,EACF;AAKA,QAAM,gBAAgB,oBAAoB,SAAS;AAAA,IACjD;AAAA,EAAA;AAGF,MAAI,CAAC,oBAAoB,WAAW,CAAC,cAAc,MAAM;AACvD,UAAM;AAAA,MACJ,KAAK;AAAA,MACL,KAAK,KAAK,OAAO;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;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,wBAAoB,QAAQ,KAAK,UAAU,KAAK,SAAS;AAAA,EAC3D;AACF;"}
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.4",
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.141.8",
63
- "@tanstack/router-generator": "1.142.0",
64
- "@tanstack/router-plugin": "1.142.0",
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.1",
68
- "@tanstack/start-server-core": "1.142.4"
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
- const toRewrite: Array<{
251
- callExpression: t.CallExpression
252
- kind: LookupKind
253
- }> = []
254
- for (const handler of candidates) {
255
- const kind = await this.resolveExprKind(handler, id)
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
- toRewrite.push({ callExpression: handler, kind: kind as LookupKind })
286
+ toRewriteMap.set(candidate, kind as LookupKind)
258
287
  }
259
288
  }
260
- if (toRewrite.length === 0) {
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
- nodePath: babel.NodePath<t.CallExpression>
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
- const found = toRewrite.findIndex((h) => path.node === h.callExpression)
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
- if (toRewrite.length > 0) {
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.map((p) => {
288
- if (p.kind === 'ServerFn') {
289
- handleCreateServerFn(p.nodePath, {
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(p.nodePath, { env: this.options.env })
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 handler = isCandidateCallExpression(
403
+ const candidate = isCandidateCallExpression(
316
404
  binding.init,
317
405
  this.validLookupKinds,
318
406
  )
319
- if (handler) {
320
- candidates.push(handler)
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
- // Try to find the export in the module's direct exports
372
- const moduleExport = importedModule.exports.get(binding.importedName)
373
-
374
- // If not found directly, check re-export-all sources (`export * from './module'`)
375
- if (!moduleExport && importedModule.reExportAllSources.length > 0) {
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 (!moduleExport) {
533
+ if (!found) {
409
534
  return 'None'
410
535
  }
411
- const importedBinding = importedModule.bindings.get(moduleExport.name)
412
- if (!importedBinding) {
413
- return 'None'
414
- }
415
- if (importedBinding.resolvedKind) {
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
- importedBinding,
421
- importedModule.id,
544
+ foundBinding,
545
+ foundModule.id,
422
546
  visited,
423
547
  )
424
- importedBinding.resolvedKind = resolvedKind
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 !== 'None') {
458
- if (calleeKind === `Root` || calleeKind === `Builder`) {
459
- return `Builder`
460
- }
461
- for (const kind of this.validLookupKinds) {
462
- if (calleeKind === kind) {
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
- ): undefined | t.CallExpression {
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
- for (const kind of lookupKinds) {
587
- if (LookupSetup[kind].candidateCallIdentifier.has(callee.property.name)) {
588
- return node
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