@tanstack/start-plugin-core 1.142.11 → 1.142.13

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.
@@ -22,6 +22,13 @@ type ExportEntry = {
22
22
  };
23
23
  type Kind = 'None' | `Root` | `Builder` | LookupKind;
24
24
  export type LookupKind = 'ServerFn' | 'Middleware' | 'IsomorphicFn' | 'ServerOnlyFn' | 'ClientOnlyFn';
25
+ export declare const KindDetectionPatterns: Record<LookupKind, RegExp>;
26
+ export declare const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>>;
27
+ /**
28
+ * Detects which LookupKinds are present in the code using string matching.
29
+ * This is a fast pre-scan before AST parsing to limit the work done during compilation.
30
+ */
31
+ export declare function detectKindsInCode(code: string, env: 'client' | 'server'): Set<LookupKind>;
25
32
  export type LookupConfig = {
26
33
  libName: string;
27
34
  rootExport: string;
@@ -38,8 +45,8 @@ export declare class ServerFnCompiler {
38
45
  private moduleCache;
39
46
  private initialized;
40
47
  private validLookupKinds;
41
- private hasDirectCallKinds;
42
- private hasRootAsCandidateKinds;
48
+ private resolveIdCache;
49
+ private exportResolutionCache;
43
50
  private knownRootImports;
44
51
  constructor(options: {
45
52
  env: 'client' | 'server';
@@ -48,8 +55,21 @@ export declare class ServerFnCompiler {
48
55
  lookupKinds: Set<LookupKind>;
49
56
  loadModule: (id: string) => Promise<void>;
50
57
  resolveId: (id: string, importer?: string) => Promise<string | null>;
58
+ /**
59
+ * In 'build' mode, resolution results are cached for performance.
60
+ * In 'dev' mode (default), caching is disabled to avoid invalidation complexity with HMR.
61
+ */
62
+ mode?: 'dev' | 'build';
51
63
  });
64
+ private get mode();
65
+ private resolveIdCached;
66
+ private getExportResolutionCache;
52
67
  private init;
68
+ /**
69
+ * Extracts bindings and exports from an already-parsed AST.
70
+ * This is the core logic shared by ingestModule and ingestModuleFromAst.
71
+ */
72
+ private extractModuleInfo;
53
73
  ingestModule({ code, id }: {
54
74
  code: string;
55
75
  id: string;
@@ -58,12 +78,13 @@ export declare class ServerFnCompiler {
58
78
  ast: import('@tanstack/router-utils').ParseAstResult;
59
79
  };
60
80
  invalidateModule(id: string): boolean;
61
- compile({ code, id, isProviderFile, }: {
81
+ compile({ code, id, isProviderFile, detectedKinds, }: {
62
82
  code: string;
63
83
  id: string;
64
84
  isProviderFile: boolean;
85
+ /** Pre-detected kinds present in this file. If not provided, all valid kinds are checked. */
86
+ detectedKinds?: Set<LookupKind>;
65
87
  }): Promise<import('@tanstack/router-utils').GeneratorResult | null>;
66
- private collectCandidates;
67
88
  private resolveIdentifierKind;
68
89
  /**
69
90
  * Recursively find an export in a module, following `export * from` chains.
@@ -24,6 +24,38 @@ const LookupSetup = {
24
24
  ServerOnlyFn: { type: "directCall" },
25
25
  ClientOnlyFn: { type: "directCall" }
26
26
  };
27
+ const KindDetectionPatterns = {
28
+ ServerFn: /\.handler\s*\(/,
29
+ Middleware: /createMiddleware/,
30
+ IsomorphicFn: /createIsomorphicFn/,
31
+ ServerOnlyFn: /createServerOnlyFn/,
32
+ ClientOnlyFn: /createClientOnlyFn/
33
+ };
34
+ const LookupKindsPerEnv = {
35
+ client: /* @__PURE__ */ new Set([
36
+ "Middleware",
37
+ "ServerFn",
38
+ "IsomorphicFn",
39
+ "ServerOnlyFn",
40
+ "ClientOnlyFn"
41
+ ]),
42
+ server: /* @__PURE__ */ new Set([
43
+ "ServerFn",
44
+ "IsomorphicFn",
45
+ "ServerOnlyFn",
46
+ "ClientOnlyFn"
47
+ ])
48
+ };
49
+ function detectKindsInCode(code, env) {
50
+ const detected = /* @__PURE__ */ new Set();
51
+ const validForEnv = LookupKindsPerEnv[env];
52
+ for (const [kind, pattern] of Object.entries(KindDetectionPatterns)) {
53
+ if (validForEnv.has(kind) && pattern.test(code)) {
54
+ detected.add(kind);
55
+ }
56
+ }
57
+ return detected;
58
+ }
27
59
  const IdentifierToKinds = /* @__PURE__ */ new Map();
28
60
  for (const [kind, setup] of Object.entries(LookupSetup)) {
29
61
  if (setup.type === "methodChain") {
@@ -37,31 +69,86 @@ for (const [kind, setup] of Object.entries(LookupSetup)) {
37
69
  }
38
70
  }
39
71
  }
72
+ const DirectCallFactoryNames = /* @__PURE__ */ new Set([
73
+ "createServerOnlyFn",
74
+ "createClientOnlyFn",
75
+ "createIsomorphicFn"
76
+ ]);
77
+ function needsDirectCallDetection(kinds) {
78
+ for (const kind of kinds) {
79
+ const setup = LookupSetup[kind];
80
+ if (setup.type === "directCall" || setup.allowRootAsCandidate) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ function areAllKindsTopLevelOnly(kinds) {
87
+ return kinds.size === 1 && kinds.has("ServerFn");
88
+ }
89
+ function isNestedDirectCallCandidate(node) {
90
+ let calleeName;
91
+ if (t.isIdentifier(node.callee)) {
92
+ calleeName = node.callee.name;
93
+ } else if (t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property)) {
94
+ calleeName = node.callee.property.name;
95
+ }
96
+ return calleeName !== void 0 && DirectCallFactoryNames.has(calleeName);
97
+ }
98
+ function isTopLevelDirectCallCandidate(path) {
99
+ const node = path.node;
100
+ const isSimpleCall = t.isIdentifier(node.callee) || t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && t.isIdentifier(node.callee.property);
101
+ if (!isSimpleCall) {
102
+ return false;
103
+ }
104
+ const parent = path.parent;
105
+ if (!t.isVariableDeclarator(parent) || parent.init !== node) {
106
+ return false;
107
+ }
108
+ const grandParent = path.parentPath.parent;
109
+ if (!t.isVariableDeclaration(grandParent)) {
110
+ return false;
111
+ }
112
+ return t.isProgram(path.parentPath.parentPath?.parent);
113
+ }
40
114
  class ServerFnCompiler {
41
115
  constructor(options) {
42
116
  this.options = options;
43
117
  this.validLookupKinds = options.lookupKinds;
44
- this.hasDirectCallKinds = false;
45
- this.hasRootAsCandidateKinds = false;
46
- for (const kind of options.lookupKinds) {
47
- const setup = LookupSetup[kind];
48
- if (setup.type === "directCall") {
49
- this.hasDirectCallKinds = true;
50
- } else if (setup.allowRootAsCandidate) {
51
- this.hasRootAsCandidateKinds = true;
52
- }
53
- }
54
118
  }
55
119
  moduleCache = /* @__PURE__ */ new Map();
56
120
  initialized = false;
57
121
  validLookupKinds;
58
- // Precomputed flags for candidate detection (avoid recomputing on each collectCandidates call)
59
- hasDirectCallKinds;
60
- hasRootAsCandidateKinds;
122
+ resolveIdCache = /* @__PURE__ */ new Map();
123
+ exportResolutionCache = /* @__PURE__ */ new Map();
61
124
  // Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
62
125
  // Maps: libName → (exportName → Kind)
63
126
  // This allows O(1) resolution for the common case without async resolveId calls
64
127
  knownRootImports = /* @__PURE__ */ new Map();
128
+ get mode() {
129
+ return this.options.mode ?? "dev";
130
+ }
131
+ async resolveIdCached(id, importer) {
132
+ if (this.mode === "dev") {
133
+ return this.options.resolveId(id, importer);
134
+ }
135
+ const cacheKey = importer ? `${importer}::${id}` : id;
136
+ const cached = this.resolveIdCache.get(cacheKey);
137
+ if (cached !== void 0) {
138
+ return cached;
139
+ }
140
+ const resolved = await this.options.resolveId(id, importer);
141
+ this.resolveIdCache.set(cacheKey, resolved);
142
+ return resolved;
143
+ }
144
+ getExportResolutionCache(moduleId) {
145
+ let cache = this.exportResolutionCache.get(moduleId);
146
+ if (!cache) {
147
+ cache = /* @__PURE__ */ new Map();
148
+ this.exportResolutionCache.set(moduleId, cache);
149
+ }
150
+ return cache;
151
+ }
65
152
  async init() {
66
153
  this.knownRootImports.set(
67
154
  "@tanstack/start-fn-stubs",
@@ -79,7 +166,7 @@ class ServerFnCompiler {
79
166
  this.knownRootImports.set(config.libName, libExports);
80
167
  }
81
168
  libExports.set(config.rootExport, config.kind);
82
- const libId = await this.options.resolveId(config.libName);
169
+ const libId = await this.resolveIdCached(config.libName);
83
170
  if (!libId) {
84
171
  throw new Error(`could not resolve "${config.libName}"`);
85
172
  }
@@ -113,8 +200,11 @@ class ServerFnCompiler {
113
200
  );
114
201
  this.initialized = true;
115
202
  }
116
- ingestModule({ code, id }) {
117
- const ast = parseAst({ code });
203
+ /**
204
+ * Extracts bindings and exports from an already-parsed AST.
205
+ * This is the core logic shared by ingestModule and ingestModuleFromAst.
206
+ */
207
+ extractModuleInfo(ast, id) {
118
208
  const bindings = /* @__PURE__ */ new Map();
119
209
  const exports = /* @__PURE__ */ new Map();
120
210
  const reExportAllSources = [];
@@ -199,6 +289,11 @@ class ServerFnCompiler {
199
289
  reExportAllSources
200
290
  };
201
291
  this.moduleCache.set(id, info);
292
+ return info;
293
+ }
294
+ ingestModule({ code, id }) {
295
+ const ast = parseAst({ code });
296
+ const info = this.extractModuleInfo(ast, id);
202
297
  return { info, ast };
203
298
  }
204
299
  invalidateModule(id) {
@@ -207,43 +302,111 @@ class ServerFnCompiler {
207
302
  async compile({
208
303
  code,
209
304
  id,
210
- isProviderFile
305
+ isProviderFile,
306
+ detectedKinds
211
307
  }) {
212
308
  if (!this.initialized) {
213
309
  await this.init();
214
310
  }
215
- const { info, ast } = this.ingestModule({ code, id });
216
- const candidates = this.collectCandidates(info.bindings);
217
- if (candidates.length === 0) {
311
+ const fileKinds = detectedKinds ? new Set([...detectedKinds].filter((k) => this.validLookupKinds.has(k))) : this.validLookupKinds;
312
+ if (fileKinds.size === 0) {
313
+ return null;
314
+ }
315
+ const checkDirectCalls = needsDirectCallDetection(fileKinds);
316
+ const canUseFastPath = areAllKindsTopLevelOnly(fileKinds);
317
+ const { ast } = this.ingestModule({ code, id });
318
+ const candidatePaths = [];
319
+ const chainCallPaths = /* @__PURE__ */ new Map();
320
+ if (canUseFastPath) {
321
+ const candidateIndices = [];
322
+ for (let i = 0; i < ast.program.body.length; i++) {
323
+ const node = ast.program.body[i];
324
+ let declarations;
325
+ if (t.isVariableDeclaration(node)) {
326
+ declarations = node.declarations;
327
+ } else if (t.isExportNamedDeclaration(node) && node.declaration) {
328
+ if (t.isVariableDeclaration(node.declaration)) {
329
+ declarations = node.declaration.declarations;
330
+ }
331
+ }
332
+ if (declarations) {
333
+ for (const decl of declarations) {
334
+ if (decl.init && t.isCallExpression(decl.init)) {
335
+ if (isMethodChainCandidate(decl.init, fileKinds)) {
336
+ candidateIndices.push(i);
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ if (candidateIndices.length === 0) {
344
+ return null;
345
+ }
346
+ babel.traverse(ast, {
347
+ Program(programPath) {
348
+ const bodyPaths = programPath.get("body");
349
+ for (const idx of candidateIndices) {
350
+ const stmtPath = bodyPaths[idx];
351
+ if (!stmtPath) continue;
352
+ stmtPath.traverse({
353
+ CallExpression(path) {
354
+ const node = path.node;
355
+ const parent = path.parent;
356
+ if (t.isMemberExpression(parent) && t.isCallExpression(path.parentPath.parent)) {
357
+ chainCallPaths.set(node, path);
358
+ return;
359
+ }
360
+ if (isMethodChainCandidate(node, fileKinds)) {
361
+ candidatePaths.push(path);
362
+ }
363
+ }
364
+ });
365
+ }
366
+ programPath.stop();
367
+ }
368
+ });
369
+ } else {
370
+ babel.traverse(ast, {
371
+ CallExpression: (path) => {
372
+ const node = path.node;
373
+ const parent = path.parent;
374
+ if (t.isMemberExpression(parent) && t.isCallExpression(path.parentPath.parent)) {
375
+ chainCallPaths.set(node, path);
376
+ return;
377
+ }
378
+ if (isMethodChainCandidate(node, fileKinds)) {
379
+ candidatePaths.push(path);
380
+ return;
381
+ }
382
+ if (checkDirectCalls) {
383
+ if (isTopLevelDirectCallCandidate(path)) {
384
+ candidatePaths.push(path);
385
+ } else if (isNestedDirectCallCandidate(node)) {
386
+ candidatePaths.push(path);
387
+ }
388
+ }
389
+ }
390
+ });
391
+ }
392
+ if (candidatePaths.length === 0) {
218
393
  return null;
219
394
  }
220
395
  const resolvedCandidates = await Promise.all(
221
- candidates.map(async (candidate) => ({
222
- candidate,
223
- kind: await this.resolveExprKind(candidate, id)
396
+ candidatePaths.map(async (path) => ({
397
+ path,
398
+ kind: await this.resolveExprKind(path.node, id)
224
399
  }))
225
400
  );
226
- const toRewriteMap = /* @__PURE__ */ new Map();
227
- for (const { candidate, kind } of resolvedCandidates) {
228
- if (this.validLookupKinds.has(kind)) {
229
- toRewriteMap.set(candidate, kind);
230
- }
231
- }
232
- if (toRewriteMap.size === 0) {
401
+ const validCandidates = resolvedCandidates.filter(
402
+ ({ kind }) => this.validLookupKinds.has(kind)
403
+ );
404
+ if (validCandidates.length === 0) {
233
405
  return null;
234
406
  }
235
407
  const pathsToRewrite = [];
236
- const callExprPaths = /* @__PURE__ */ new Map();
237
- babel.traverse(ast, {
238
- CallExpression(path) {
239
- callExprPaths.set(path.node, path);
240
- }
241
- });
242
- for (const [node, kind] of toRewriteMap) {
243
- const path = callExprPaths.get(node);
244
- if (!path) {
245
- continue;
246
- }
408
+ for (const { path, kind } of validCandidates) {
409
+ const node = path.node;
247
410
  const methodChain = {
248
411
  middleware: null,
249
412
  inputValidator: null,
@@ -252,6 +415,7 @@ class ServerFnCompiler {
252
415
  client: null
253
416
  };
254
417
  let currentNode = node;
418
+ let currentPath = path;
255
419
  while (true) {
256
420
  const callee = currentNode.callee;
257
421
  if (!t.isMemberExpression(callee)) {
@@ -260,7 +424,6 @@ class ServerFnCompiler {
260
424
  if (t.isIdentifier(callee.property)) {
261
425
  const name = callee.property.name;
262
426
  if (name in methodChain) {
263
- const currentPath = callExprPaths.get(currentNode);
264
427
  const args = currentPath.get("arguments");
265
428
  const firstArgPath = Array.isArray(args) && args.length > 0 ? args[0] ?? null : null;
266
429
  methodChain[name] = {
@@ -273,14 +436,14 @@ class ServerFnCompiler {
273
436
  break;
274
437
  }
275
438
  currentNode = callee.object;
439
+ const nextPath = chainCallPaths.get(currentNode);
440
+ if (!nextPath) {
441
+ break;
442
+ }
443
+ currentPath = nextPath;
276
444
  }
277
445
  pathsToRewrite.push({ path, kind, methodChain });
278
446
  }
279
- if (pathsToRewrite.length !== toRewriteMap.size) {
280
- throw new Error(
281
- `Internal error: could not find all paths to rewrite. please file an issue`
282
- );
283
- }
284
447
  const refIdents = findReferencedIdentifiers(ast);
285
448
  for (const { path, kind, methodChain } of pathsToRewrite) {
286
449
  const candidate = { path, methodChain };
@@ -313,28 +476,6 @@ class ServerFnCompiler {
313
476
  filename: id
314
477
  });
315
478
  }
316
- // collects all candidate CallExpressions at top-level
317
- collectCandidates(bindings) {
318
- const candidates = [];
319
- for (const binding of bindings.values()) {
320
- if (binding.type === "var" && t.isCallExpression(binding.init)) {
321
- const methodChainCandidate = isCandidateCallExpression(
322
- binding.init,
323
- this.validLookupKinds
324
- );
325
- if (methodChainCandidate) {
326
- candidates.push(methodChainCandidate);
327
- continue;
328
- }
329
- if (this.hasDirectCallKinds || this.hasRootAsCandidateKinds) {
330
- if (t.isIdentifier(binding.init.callee) || t.isMemberExpression(binding.init.callee) && t.isIdentifier(binding.init.callee.property)) {
331
- candidates.push(binding.init);
332
- }
333
- }
334
- }
335
- }
336
- return candidates;
337
- }
338
479
  async resolveIdentifierKind(ident, id, visited = /* @__PURE__ */ new Set()) {
339
480
  const info = await this.getModuleInfo(id);
340
481
  const binding = info.bindings.get(ident);
@@ -358,6 +499,16 @@ class ServerFnCompiler {
358
499
  * Returns the module info and binding if found, or undefined if not found.
359
500
  */
360
501
  async findExportInModule(moduleInfo, exportName, visitedModules = /* @__PURE__ */ new Set()) {
502
+ const isBuildMode = this.mode === "build";
503
+ if (isBuildMode && visitedModules.size === 0) {
504
+ const moduleCache = this.exportResolutionCache.get(moduleInfo.id);
505
+ if (moduleCache) {
506
+ const cached = moduleCache.get(exportName);
507
+ if (cached !== void 0) {
508
+ return cached ?? void 0;
509
+ }
510
+ }
511
+ }
361
512
  if (visitedModules.has(moduleInfo.id)) {
362
513
  return void 0;
363
514
  }
@@ -366,13 +517,17 @@ class ServerFnCompiler {
366
517
  if (directExport) {
367
518
  const binding = moduleInfo.bindings.get(directExport.name);
368
519
  if (binding) {
369
- return { moduleInfo, binding };
520
+ const result = { moduleInfo, binding };
521
+ if (isBuildMode) {
522
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, result);
523
+ }
524
+ return result;
370
525
  }
371
526
  }
372
527
  if (moduleInfo.reExportAllSources.length > 0) {
373
528
  const results = await Promise.all(
374
529
  moduleInfo.reExportAllSources.map(async (reExportSource) => {
375
- const reExportTarget = await this.options.resolveId(
530
+ const reExportTarget = await this.resolveIdCached(
376
531
  reExportSource,
377
532
  moduleInfo.id
378
533
  );
@@ -389,10 +544,16 @@ class ServerFnCompiler {
389
544
  );
390
545
  for (const result of results) {
391
546
  if (result) {
547
+ if (isBuildMode) {
548
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, result);
549
+ }
392
550
  return result;
393
551
  }
394
552
  }
395
553
  }
554
+ if (isBuildMode) {
555
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, null);
556
+ }
396
557
  return void 0;
397
558
  }
398
559
  async resolveBindingKind(binding, fileId, visited = /* @__PURE__ */ new Set()) {
@@ -408,7 +569,7 @@ class ServerFnCompiler {
408
569
  return kind;
409
570
  }
410
571
  }
411
- const target = await this.options.resolveId(binding.source, fileId);
572
+ const target = await this.resolveIdCached(binding.source, fileId);
412
573
  if (!target) {
413
574
  return "None";
414
575
  }
@@ -501,7 +662,7 @@ class ServerFnCompiler {
501
662
  const info = await this.getModuleInfo(fileId);
502
663
  const binding = info.bindings.get(callee.object.name);
503
664
  if (binding && binding.type === "import" && binding.importedName === "*") {
504
- const targetModuleId = await this.options.resolveId(
665
+ const targetModuleId = await this.resolveIdCached(
505
666
  binding.source,
506
667
  fileId
507
668
  );
@@ -542,23 +703,25 @@ class ServerFnCompiler {
542
703
  return cached;
543
704
  }
544
705
  }
545
- function isCandidateCallExpression(node, lookupKinds) {
546
- if (!t.isCallExpression(node)) return void 0;
706
+ function isMethodChainCandidate(node, lookupKinds) {
547
707
  const callee = node.callee;
548
708
  if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) {
549
- return void 0;
709
+ return false;
550
710
  }
551
711
  const possibleKinds = IdentifierToKinds.get(callee.property.name);
552
712
  if (possibleKinds) {
553
713
  for (const kind of possibleKinds) {
554
714
  if (lookupKinds.has(kind)) {
555
- return node;
715
+ return true;
556
716
  }
557
717
  }
558
718
  }
559
- return void 0;
719
+ return false;
560
720
  }
561
721
  export {
562
- ServerFnCompiler
722
+ KindDetectionPatterns,
723
+ LookupKindsPerEnv,
724
+ ServerFnCompiler,
725
+ detectKindsInCode
563
726
  };
564
727
  //# sourceMappingURL=compiler.js.map