Skip to content

Commit 99c1058

Browse files
KyleAMathewsclaude
andcommitted
fix(router-plugin): handle partial declarator exports + add edge case tests
Based on comprehensive code review feedback, this commit addresses: **Bug Fix: Partial Declarators** When multiple variables are declared in same statement (const a = 1, b = 2), and only some are shared between split/non-split parts, we now correctly: - Export only the shared variables - Keep non-shared variables local in their respective files - Split the declaration into separate statements when needed Before: `export const a = 1, shared = new Map()` (both exported) After: `const a = 1; export const shared = new Map()` (only shared exported) **New Edge Case Tests (72 total across React/Solid):** 1. Split-only variable stays local (validates main correctness guard) 2. Partial declarators - only shared ones exported 3. let with reassignment (live binding behavior) 4. Destructuring precision (promotes correct binding) 5. Alias chains (tracks through variable aliases) 6. Nested closures (finds references in function bodies) These tests ensure the implementation correctly handles complex variable sharing scenarios and only promotes/imports variables that are genuinely shared between split and non-split parts. All 360 tests passing. Reviewer feedback incorporated from PR TanStack#5767 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 4b53742 commit 99c1058

File tree

135 files changed

+1419
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+1419
-17
lines changed

packages/router-plugin/src/core/code-splitter/compilers.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@ export function compileCodeSplitReferenceRoute(
529529
*/
530530
if (sharedModuleLevelIdents.size > 0) {
531531
modified = true
532+
533+
// Track which variable declarations we've already processed to avoid duplicate processing
534+
const processedVarDecls = new Set<babel.NodePath<t.VariableDeclaration>>()
535+
532536
sharedModuleLevelIdents.forEach((identName) => {
533537
const binding = programPath.scope.getBinding(identName)
534538
if (!binding) return
@@ -541,19 +545,57 @@ export function compileCodeSplitReferenceRoute(
541545
bindingPath.parentPath?.isVariableDeclaration() &&
542546
bindingPath.parentPath.parentPath?.isProgram()
543547
) {
544-
const varDecl = bindingPath.parentPath
548+
const varDecl = bindingPath.parentPath as babel.NodePath<t.VariableDeclaration>
545549

546550
// Only export const/let declarations (not imports or functions)
547551
if (
548552
varDecl.node.kind === 'const' ||
549553
varDecl.node.kind === 'let'
550554
) {
551-
// Convert to export declaration
552-
const exportDecl = t.exportNamedDeclaration(varDecl.node, [])
553-
varDecl.replaceWith(exportDecl)
554-
knownExportedIdents.add(identName)
555-
// Track in sharedExports for virtual compiler to use
556-
opts.sharedExports?.add(identName)
555+
// Skip if we've already processed this declaration
556+
if (processedVarDecls.has(varDecl)) {
557+
return
558+
}
559+
processedVarDecls.add(varDecl)
560+
561+
// Check if this declaration has multiple declarators
562+
const declarators = varDecl.node.declarations
563+
if (declarators.length > 1) {
564+
// Split declarators: shared ones get exported, others stay local
565+
const sharedDeclarators: Array<t.VariableDeclarator> = []
566+
const localDeclarators: Array<t.VariableDeclarator> = []
567+
568+
declarators.forEach((declarator) => {
569+
if (t.isIdentifier(declarator.id) && sharedModuleLevelIdents.has(declarator.id.name)) {
570+
sharedDeclarators.push(declarator)
571+
knownExportedIdents.add(declarator.id.name)
572+
opts.sharedExports?.add(declarator.id.name)
573+
} else {
574+
localDeclarators.push(declarator)
575+
}
576+
})
577+
578+
// Replace with split declarations
579+
if (sharedDeclarators.length > 0 && localDeclarators.length > 0) {
580+
// Both shared and local declarators
581+
const localVarDecl = t.variableDeclaration(varDecl.node.kind, localDeclarators)
582+
const sharedVarDecl = t.variableDeclaration(varDecl.node.kind, sharedDeclarators)
583+
const exportDecl = t.exportNamedDeclaration(sharedVarDecl, [])
584+
585+
varDecl.replaceWithMultiple([localVarDecl, exportDecl])
586+
} else if (sharedDeclarators.length > 0) {
587+
// All declarators are shared
588+
const sharedVarDecl = t.variableDeclaration(varDecl.node.kind, sharedDeclarators)
589+
const exportDecl = t.exportNamedDeclaration(sharedVarDecl, [])
590+
varDecl.replaceWith(exportDecl)
591+
}
592+
} else {
593+
// Single declarator - export the whole thing
594+
const exportDecl = t.exportNamedDeclaration(varDecl.node, [])
595+
varDecl.replaceWith(exportDecl)
596+
knownExportedIdents.add(identName)
597+
opts.sharedExports?.add(identName)
598+
}
557599
}
558600
}
559601
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
console.warn("[tanstack-router] These exports from \"shared-alias-chain.tsx\" will not be code-split and will increase your bundle size:\n- alias\nFor the best optimization, these items should either have their export statements removed, or be imported from another location that is not a route file.");
2+
const $$splitComponentImporter = () => import('./shared-alias-chain.tsx?tsr-split=component');
3+
import { lazyRouteComponent } from '@tanstack/react-router';
4+
import { createFileRoute } from '@tanstack/react-router';
5+
6+
// Alias chain - ensure we track through aliases
7+
const base = {
8+
name: 'collection',
9+
items: []
10+
};
11+
export const alias = base;
12+
export const Route = createFileRoute('/test')({
13+
loader: async () => {
14+
return alias.items;
15+
},
16+
component: lazyRouteComponent($$splitComponentImporter, 'component')
17+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { alias } from "./shared-alias-chain.tsx"; // Alias chain - ensure we track through aliases
2+
function TestComponent() {
3+
return <div>{alias.name}</div>;
4+
}
5+
export { TestComponent as component };

packages/router-plugin/tests/code-splitter/snapshots/react/1-default/[email protected]

Whitespace-only changes.

packages/router-plugin/tests/code-splitter/snapshots/react/1-default/[email protected]

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
console.warn("[tanstack-router] These exports from \"shared-destructuring.tsx\" will not be code-split and will increase your bundle size:\n- apiUrl\nFor the best optimization, these items should either have their export statements removed, or be imported from another location that is not a route file.");
2+
const $$splitComponentImporter = () => import('./shared-destructuring.tsx?tsr-split=component');
3+
import { lazyRouteComponent } from '@tanstack/react-router';
4+
import { createFileRoute } from '@tanstack/react-router';
5+
6+
// Destructuring - ensure we promote the right binding
7+
const cfg = {
8+
apiUrl: 'http://api.com',
9+
timeout: 5000
10+
};
11+
export const {
12+
apiUrl
13+
} = cfg;
14+
export const Route = createFileRoute('/test')({
15+
loader: async () => {
16+
// Uses the destructured binding
17+
return fetch(apiUrl).then(r => r.json());
18+
},
19+
component: lazyRouteComponent($$splitComponentImporter, 'component')
20+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Destructuring - ensure we promote the right binding
2+
const cfg = {
3+
apiUrl: 'http://api.com',
4+
timeout: 5000
5+
};
6+
const {
7+
apiUrl
8+
} = cfg;
9+
function TestComponent() {
10+
// Also uses the destructured binding
11+
return <div>API: {apiUrl}</div>;
12+
}
13+
export { TestComponent as component };

packages/router-plugin/tests/code-splitter/snapshots/react/1-default/[email protected]

Whitespace-only changes.

packages/router-plugin/tests/code-splitter/snapshots/react/1-default/[email protected]

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
console.warn("[tanstack-router] These exports from \"shared-let-reassignment.tsx\" will not be code-split and will increase your bundle size:\n- store\nFor the best optimization, these items should either have their export statements removed, or be imported from another location that is not a route file.");
2+
const $$splitComponentImporter = () => import('./shared-let-reassignment.tsx?tsr-split=component');
3+
import { lazyRouteComponent } from '@tanstack/react-router';
4+
import { createFileRoute } from '@tanstack/react-router';
5+
6+
// let with reassignment - tests live binding behavior
7+
export let store = {
8+
count: 0
9+
};
10+
store = {
11+
count: 1,
12+
updated: true
13+
};
14+
export const Route = createFileRoute('/test')({
15+
loader: async () => {
16+
store.count++;
17+
return {
18+
data: 'loaded'
19+
};
20+
},
21+
component: lazyRouteComponent($$splitComponentImporter, 'component')
22+
});

0 commit comments

Comments
 (0)