@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.
- package/dist/esm/create-server-fn-plugin/compiler.d.ts +16 -1
- package/dist/esm/create-server-fn-plugin/compiler.js +200 -29
- package/dist/esm/create-server-fn-plugin/compiler.js.map +1 -1
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.d.ts +21 -0
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.js +10 -0
- package/dist/esm/create-server-fn-plugin/handleClientOnlyJSX.js.map +1 -0
- package/dist/esm/create-server-fn-plugin/plugin.js +10 -1
- package/dist/esm/create-server-fn-plugin/plugin.js.map +1 -1
- package/package.json +6 -6
- package/src/create-server-fn-plugin/compiler.ts +311 -38
- package/src/create-server-fn-plugin/handleClientOnlyJSX.ts +32 -0
- package/src/create-server-fn-plugin/plugin.ts +13 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 (
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
727
|
+
const targetModuleId = await this.resolveIdCached(
|
|
557
728
|
binding.source,
|
|
558
729
|
fileId
|
|
559
730
|
);
|