@tanstack/start-plugin-core 1.142.12 → 1.143.0

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.
@@ -21,7 +21,7 @@ type ExportEntry = {
21
21
  targetId: string;
22
22
  };
23
23
  type Kind = 'None' | `Root` | `Builder` | LookupKind;
24
- export type LookupKind = 'ServerFn' | 'Middleware' | 'IsomorphicFn' | 'ServerOnlyFn' | 'ClientOnlyFn';
24
+ export type LookupKind = 'ServerFn' | 'Middleware' | 'IsomorphicFn' | 'ServerOnlyFn' | 'ClientOnlyFn' | 'ClientOnlyJSX';
25
25
  export declare const KindDetectionPatterns: Record<LookupKind, RegExp>;
26
26
  export declare const LookupKindsPerEnv: Record<'client' | 'server', Set<LookupKind>>;
27
27
  /**
@@ -45,6 +45,8 @@ export declare class ServerFnCompiler {
45
45
  private moduleCache;
46
46
  private initialized;
47
47
  private validLookupKinds;
48
+ private resolveIdCache;
49
+ private exportResolutionCache;
48
50
  private knownRootImports;
49
51
  constructor(options: {
50
52
  env: 'client' | 'server';
@@ -53,8 +55,21 @@ export declare class ServerFnCompiler {
53
55
  lookupKinds: Set<LookupKind>;
54
56
  loadModule: (id: string) => Promise<void>;
55
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';
56
63
  });
64
+ private get mode();
65
+ private resolveIdCached;
66
+ private getExportResolutionCache;
57
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;
58
73
  ingestModule({ code, id }: {
59
74
  code: string;
60
75
  id: string;
@@ -6,6 +6,7 @@ import { handleCreateServerFn } from "./handleCreateServerFn.js";
6
6
  import { handleCreateMiddleware } from "./handleCreateMiddleware.js";
7
7
  import { handleCreateIsomorphicFn } from "./handleCreateIsomorphicFn.js";
8
8
  import { handleEnvOnlyFn } from "./handleEnvOnly.js";
9
+ import { handleClientOnlyJSX } from "./handleClientOnlyJSX.js";
9
10
  const LookupSetup = {
10
11
  ServerFn: {
11
12
  type: "methodChain",
@@ -22,14 +23,16 @@ const LookupSetup = {
22
23
  // createIsomorphicFn() alone is valid (returns no-op)
23
24
  },
24
25
  ServerOnlyFn: { type: "directCall" },
25
- ClientOnlyFn: { type: "directCall" }
26
+ ClientOnlyFn: { type: "directCall" },
27
+ ClientOnlyJSX: { type: "jsx", componentName: "ClientOnly" }
26
28
  };
27
29
  const KindDetectionPatterns = {
28
30
  ServerFn: /\.handler\s*\(/,
29
31
  Middleware: /createMiddleware/,
30
32
  IsomorphicFn: /createIsomorphicFn/,
31
33
  ServerOnlyFn: /createServerOnlyFn/,
32
- ClientOnlyFn: /createClientOnlyFn/
34
+ ClientOnlyFn: /createClientOnlyFn/,
35
+ ClientOnlyJSX: /<ClientOnly|import\s*\{[^}]*\bClientOnly\b/
33
36
  };
34
37
  const LookupKindsPerEnv = {
35
38
  client: /* @__PURE__ */ new Set([
@@ -43,7 +46,9 @@ const LookupKindsPerEnv = {
43
46
  "ServerFn",
44
47
  "IsomorphicFn",
45
48
  "ServerOnlyFn",
46
- "ClientOnlyFn"
49
+ "ClientOnlyFn",
50
+ "ClientOnlyJSX"
51
+ // Only transform on server to remove children
47
52
  ])
48
53
  };
49
54
  function detectKindsInCode(code, env) {
@@ -77,12 +82,34 @@ const DirectCallFactoryNames = /* @__PURE__ */ new Set([
77
82
  function needsDirectCallDetection(kinds) {
78
83
  for (const kind of kinds) {
79
84
  const setup = LookupSetup[kind];
80
- if (setup.type === "directCall" || setup.allowRootAsCandidate) {
85
+ if (setup.type === "directCall" || setup.type === "methodChain" && setup.allowRootAsCandidate) {
81
86
  return true;
82
87
  }
83
88
  }
84
89
  return false;
85
90
  }
91
+ function areAllKindsTopLevelOnly(kinds) {
92
+ return kinds.size === 1 && kinds.has("ServerFn");
93
+ }
94
+ function needsJSXDetection(kinds) {
95
+ for (const kind of kinds) {
96
+ const setup = LookupSetup[kind];
97
+ if (setup.type === "jsx") {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+ function getJSXComponentNames(kinds) {
104
+ const names = /* @__PURE__ */ new Set();
105
+ for (const kind of kinds) {
106
+ const setup = LookupSetup[kind];
107
+ if (setup.type === "jsx") {
108
+ names.add(setup.componentName);
109
+ }
110
+ }
111
+ return names;
112
+ }
86
113
  function isNestedDirectCallCandidate(node) {
87
114
  let calleeName;
88
115
  if (t.isIdentifier(node.callee)) {
@@ -116,10 +143,36 @@ class ServerFnCompiler {
116
143
  moduleCache = /* @__PURE__ */ new Map();
117
144
  initialized = false;
118
145
  validLookupKinds;
146
+ resolveIdCache = /* @__PURE__ */ new Map();
147
+ exportResolutionCache = /* @__PURE__ */ new Map();
119
148
  // Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
120
149
  // Maps: libName → (exportName → Kind)
121
150
  // This allows O(1) resolution for the common case without async resolveId calls
122
151
  knownRootImports = /* @__PURE__ */ new Map();
152
+ get mode() {
153
+ return this.options.mode ?? "dev";
154
+ }
155
+ async resolveIdCached(id, importer) {
156
+ if (this.mode === "dev") {
157
+ return this.options.resolveId(id, importer);
158
+ }
159
+ const cacheKey = importer ? `${importer}::${id}` : id;
160
+ const cached = this.resolveIdCache.get(cacheKey);
161
+ if (cached !== void 0) {
162
+ return cached;
163
+ }
164
+ const resolved = await this.options.resolveId(id, importer);
165
+ this.resolveIdCache.set(cacheKey, resolved);
166
+ return resolved;
167
+ }
168
+ getExportResolutionCache(moduleId) {
169
+ let cache = this.exportResolutionCache.get(moduleId);
170
+ if (!cache) {
171
+ cache = /* @__PURE__ */ new Map();
172
+ this.exportResolutionCache.set(moduleId, cache);
173
+ }
174
+ return cache;
175
+ }
123
176
  async init() {
124
177
  this.knownRootImports.set(
125
178
  "@tanstack/start-fn-stubs",
@@ -137,7 +190,13 @@ class ServerFnCompiler {
137
190
  this.knownRootImports.set(config.libName, libExports);
138
191
  }
139
192
  libExports.set(config.rootExport, config.kind);
140
- const libId = await this.options.resolveId(config.libName);
193
+ if (config.kind !== "Root") {
194
+ const setup = LookupSetup[config.kind];
195
+ if (setup.type === "jsx") {
196
+ return;
197
+ }
198
+ }
199
+ const libId = await this.resolveIdCached(config.libName);
141
200
  if (!libId) {
142
201
  throw new Error(`could not resolve "${config.libName}"`);
143
202
  }
@@ -171,8 +230,11 @@ class ServerFnCompiler {
171
230
  );
172
231
  this.initialized = true;
173
232
  }
174
- ingestModule({ code, id }) {
175
- const ast = parseAst({ code });
233
+ /**
234
+ * Extracts bindings and exports from an already-parsed AST.
235
+ * This is the core logic shared by ingestModule and ingestModuleFromAst.
236
+ */
237
+ extractModuleInfo(ast, id) {
176
238
  const bindings = /* @__PURE__ */ new Map();
177
239
  const exports = /* @__PURE__ */ new Map();
178
240
  const reExportAllSources = [];
@@ -257,6 +319,11 @@ class ServerFnCompiler {
257
319
  reExportAllSources
258
320
  };
259
321
  this.moduleCache.set(id, info);
322
+ return info;
323
+ }
324
+ ingestModule({ code, id }) {
325
+ const ast = parseAst({ code });
326
+ const info = this.extractModuleInfo(ast, id);
260
327
  return { info, ast };
261
328
  }
262
329
  invalidateModule(id) {
@@ -276,31 +343,102 @@ class ServerFnCompiler {
276
343
  return null;
277
344
  }
278
345
  const checkDirectCalls = needsDirectCallDetection(fileKinds);
346
+ const canUseFastPath = areAllKindsTopLevelOnly(fileKinds);
279
347
  const { ast } = this.ingestModule({ code, id });
280
348
  const candidatePaths = [];
281
349
  const chainCallPaths = /* @__PURE__ */ new Map();
282
- babel.traverse(ast, {
283
- CallExpression: (path) => {
284
- const node = path.node;
285
- const parent = path.parent;
286
- if (t.isMemberExpression(parent) && t.isCallExpression(path.parentPath.parent)) {
287
- chainCallPaths.set(node, path);
288
- return;
350
+ const jsxCandidatePaths = [];
351
+ const checkJSX = needsJSXDetection(fileKinds);
352
+ const jsxTargetComponentNames = checkJSX ? getJSXComponentNames(fileKinds) : null;
353
+ const moduleInfo = this.moduleCache.get(id);
354
+ if (canUseFastPath) {
355
+ const candidateIndices = [];
356
+ for (let i = 0; i < ast.program.body.length; i++) {
357
+ const node = ast.program.body[i];
358
+ let declarations;
359
+ if (t.isVariableDeclaration(node)) {
360
+ declarations = node.declarations;
361
+ } else if (t.isExportNamedDeclaration(node) && node.declaration) {
362
+ if (t.isVariableDeclaration(node.declaration)) {
363
+ declarations = node.declaration.declarations;
364
+ }
289
365
  }
290
- if (isMethodChainCandidate(node, fileKinds)) {
291
- candidatePaths.push(path);
292
- return;
366
+ if (declarations) {
367
+ for (const decl of declarations) {
368
+ if (decl.init && t.isCallExpression(decl.init)) {
369
+ if (isMethodChainCandidate(decl.init, fileKinds)) {
370
+ candidateIndices.push(i);
371
+ break;
372
+ }
373
+ }
374
+ }
293
375
  }
294
- if (checkDirectCalls) {
295
- if (isTopLevelDirectCallCandidate(path)) {
296
- candidatePaths.push(path);
297
- } else if (isNestedDirectCallCandidate(node)) {
376
+ }
377
+ if (candidateIndices.length === 0) {
378
+ return null;
379
+ }
380
+ babel.traverse(ast, {
381
+ Program(programPath) {
382
+ const bodyPaths = programPath.get("body");
383
+ for (const idx of candidateIndices) {
384
+ const stmtPath = bodyPaths[idx];
385
+ if (!stmtPath) continue;
386
+ stmtPath.traverse({
387
+ CallExpression(path) {
388
+ const node = path.node;
389
+ const parent = path.parent;
390
+ if (t.isMemberExpression(parent) && t.isCallExpression(path.parentPath.parent)) {
391
+ chainCallPaths.set(node, path);
392
+ return;
393
+ }
394
+ if (isMethodChainCandidate(node, fileKinds)) {
395
+ candidatePaths.push(path);
396
+ }
397
+ }
398
+ });
399
+ }
400
+ programPath.stop();
401
+ }
402
+ });
403
+ } else {
404
+ babel.traverse(ast, {
405
+ CallExpression: (path) => {
406
+ const node = path.node;
407
+ const parent = path.parent;
408
+ if (t.isMemberExpression(parent) && t.isCallExpression(path.parentPath.parent)) {
409
+ chainCallPaths.set(node, path);
410
+ return;
411
+ }
412
+ if (isMethodChainCandidate(node, fileKinds)) {
298
413
  candidatePaths.push(path);
414
+ return;
415
+ }
416
+ if (checkDirectCalls) {
417
+ if (isTopLevelDirectCallCandidate(path)) {
418
+ candidatePaths.push(path);
419
+ } else if (isNestedDirectCallCandidate(node)) {
420
+ candidatePaths.push(path);
421
+ }
422
+ }
423
+ },
424
+ // Pattern 3: JSX element pattern (e.g., <ClientOnly>)
425
+ // Collect JSX elements where the component name matches a known import
426
+ // that resolves to a target component (e.g., ClientOnly from @tanstack/react-router)
427
+ JSXElement: (path) => {
428
+ if (!checkJSX || !jsxTargetComponentNames) return;
429
+ const openingElement = path.node.openingElement;
430
+ const nameNode = openingElement.name;
431
+ if (!t.isJSXIdentifier(nameNode)) return;
432
+ const componentName = nameNode.name;
433
+ const binding = moduleInfo.bindings.get(componentName);
434
+ if (!binding || binding.type !== "import") return;
435
+ if (jsxTargetComponentNames.has(binding.importedName)) {
436
+ jsxCandidatePaths.push(path);
299
437
  }
300
438
  }
301
- }
302
- });
303
- if (candidatePaths.length === 0) {
439
+ });
440
+ }
441
+ if (candidatePaths.length === 0 && jsxCandidatePaths.length === 0) {
304
442
  return null;
305
443
  }
306
444
  const resolvedCandidates = await Promise.all(
@@ -312,7 +450,7 @@ class ServerFnCompiler {
312
450
  const validCandidates = resolvedCandidates.filter(
313
451
  ({ kind }) => this.validLookupKinds.has(kind)
314
452
  );
315
- if (validCandidates.length === 0) {
453
+ if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) {
316
454
  return null;
317
455
  }
318
456
  const pathsToRewrite = [];
@@ -380,6 +518,19 @@ class ServerFnCompiler {
380
518
  });
381
519
  }
382
520
  }
521
+ for (const jsxPath of jsxCandidatePaths) {
522
+ const openingElement = jsxPath.node.openingElement;
523
+ const nameNode = openingElement.name;
524
+ if (!t.isJSXIdentifier(nameNode)) continue;
525
+ const componentName = nameNode.name;
526
+ const binding = moduleInfo.bindings.get(componentName);
527
+ if (!binding || binding.type !== "import") continue;
528
+ const knownExports = this.knownRootImports.get(binding.source);
529
+ if (!knownExports) continue;
530
+ const kind = knownExports.get(binding.importedName);
531
+ if (kind !== "ClientOnlyJSX") continue;
532
+ handleClientOnlyJSX(jsxPath);
533
+ }
383
534
  deadCodeElimination(ast, refIdents);
384
535
  return generateFromAst(ast, {
385
536
  sourceMaps: true,
@@ -410,6 +561,16 @@ class ServerFnCompiler {
410
561
  * Returns the module info and binding if found, or undefined if not found.
411
562
  */
412
563
  async findExportInModule(moduleInfo, exportName, visitedModules = /* @__PURE__ */ new Set()) {
564
+ const isBuildMode = this.mode === "build";
565
+ if (isBuildMode && visitedModules.size === 0) {
566
+ const moduleCache = this.exportResolutionCache.get(moduleInfo.id);
567
+ if (moduleCache) {
568
+ const cached = moduleCache.get(exportName);
569
+ if (cached !== void 0) {
570
+ return cached ?? void 0;
571
+ }
572
+ }
573
+ }
413
574
  if (visitedModules.has(moduleInfo.id)) {
414
575
  return void 0;
415
576
  }
@@ -418,13 +579,17 @@ class ServerFnCompiler {
418
579
  if (directExport) {
419
580
  const binding = moduleInfo.bindings.get(directExport.name);
420
581
  if (binding) {
421
- return { moduleInfo, binding };
582
+ const result = { moduleInfo, binding };
583
+ if (isBuildMode) {
584
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, result);
585
+ }
586
+ return result;
422
587
  }
423
588
  }
424
589
  if (moduleInfo.reExportAllSources.length > 0) {
425
590
  const results = await Promise.all(
426
591
  moduleInfo.reExportAllSources.map(async (reExportSource) => {
427
- const reExportTarget = await this.options.resolveId(
592
+ const reExportTarget = await this.resolveIdCached(
428
593
  reExportSource,
429
594
  moduleInfo.id
430
595
  );
@@ -441,10 +606,16 @@ class ServerFnCompiler {
441
606
  );
442
607
  for (const result of results) {
443
608
  if (result) {
609
+ if (isBuildMode) {
610
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, result);
611
+ }
444
612
  return result;
445
613
  }
446
614
  }
447
615
  }
616
+ if (isBuildMode) {
617
+ this.getExportResolutionCache(moduleInfo.id).set(exportName, null);
618
+ }
448
619
  return void 0;
449
620
  }
450
621
  async resolveBindingKind(binding, fileId, visited = /* @__PURE__ */ new Set()) {
@@ -460,7 +631,7 @@ class ServerFnCompiler {
460
631
  return kind;
461
632
  }
462
633
  }
463
- const target = await this.options.resolveId(binding.source, fileId);
634
+ const target = await this.resolveIdCached(binding.source, fileId);
464
635
  if (!target) {
465
636
  return "None";
466
637
  }
@@ -553,7 +724,7 @@ class ServerFnCompiler {
553
724
  const info = await this.getModuleInfo(fileId);
554
725
  const binding = info.bindings.get(callee.object.name);
555
726
  if (binding && binding.type === "import" && binding.importedName === "*") {
556
- const targetModuleId = await this.options.resolveId(
727
+ const targetModuleId = await this.resolveIdCached(
557
728
  binding.source,
558
729
  fileId
559
730
  );