@@ -118,6 +118,7 @@ export function compileCodeSplitReferenceRoute(
118118 const refIdents = findReferencedIdentifiers ( ast )
119119
120120 const knownExportedIdents = new Set < string > ( )
121+ const sharedModuleLevelIdents = new Set < string > ( )
121122
122123 function findIndexForSplitNode ( str : string ) {
123124 return opts . codeSplitGroupings . findIndex ( ( group ) =>
@@ -165,6 +166,52 @@ export function compileCodeSplitReferenceRoute(
165166 return programPath . scope . hasBinding ( name )
166167 }
167168
169+ // Helper to collect all identifiers referenced by a node
170+ const collectReferencedIdentifiers = ( propPath : babel . NodePath < t . ObjectProperty > ) : Set < string > => {
171+ const identifiers = new Set < string > ( )
172+ const valuePath = propPath . get ( 'value' )
173+
174+ // If the value is an identifier, we need to follow it to its definition
175+ const pathsToAnalyze : Array < babel . NodePath > = [ ]
176+
177+ if ( valuePath . isIdentifier ( ) ) {
178+ const binding = programPath . scope . getBinding ( valuePath . node . name )
179+ if ( binding ) {
180+ pathsToAnalyze . push ( binding . path )
181+ }
182+ } else {
183+ pathsToAnalyze . push ( valuePath )
184+ }
185+
186+ // Traverse each path to find all referenced identifiers
187+ pathsToAnalyze . forEach ( analyzePath => {
188+ analyzePath . traverse ( {
189+ Identifier ( idPath ) {
190+ // Only collect identifiers that are references (not declarations)
191+ if ( ! idPath . isReferencedIdentifier ( ) ) return
192+
193+ const name = idPath . node . name
194+ const binding = programPath . scope . getBinding ( name )
195+
196+ // Only include identifiers that are defined at module level
197+ if ( binding ) {
198+ const declarationPath = binding . path
199+ // Check if this is a top-level variable declaration
200+ if (
201+ declarationPath . isVariableDeclarator ( ) &&
202+ declarationPath . parentPath ?. isVariableDeclaration ( ) &&
203+ declarationPath . parentPath . parentPath ?. isProgram ( )
204+ ) {
205+ identifiers . add ( name )
206+ }
207+ }
208+ } ,
209+ } )
210+ } )
211+
212+ return identifiers
213+ }
214+
168215 if ( t . isObjectExpression ( routeOptions ) ) {
169216 if ( opts . deleteNodes && opts . deleteNodes . size > 0 ) {
170217 routeOptions . properties = routeOptions . properties . filter (
@@ -191,6 +238,41 @@ export function compileCodeSplitReferenceRoute(
191238 // exit traversal so this route is not split
192239 return programPath . stop ( )
193240 }
241+
242+ // First pass: collect identifiers used by split vs non-split properties
243+ const splitPropertyIdents = new Set < string > ( )
244+ const nonSplitPropertyIdents = new Set < string > ( )
245+
246+ // We need to analyze the route options object to find shared identifiers
247+ // Since routeOptions might be a resolved node, we traverse from programPath
248+ // to find all ObjectProperty paths and analyze them
249+ programPath . traverse ( {
250+ ObjectProperty ( propPath ) {
251+ // Check if this property belongs to the route options by checking its parent
252+ if ( ! t . isObjectExpression ( propPath . parent ) ) return
253+ if ( propPath . parent !== routeOptions ) return
254+ if ( ! t . isIdentifier ( propPath . node . key ) ) return
255+
256+ const key = propPath . node . key . name
257+ const willBeSplit = findIndexForSplitNode ( key ) !== - 1 && SPLIT_NODES_CONFIG . has ( key as any )
258+
259+ const idents = collectReferencedIdentifiers ( propPath )
260+
261+ if ( willBeSplit ) {
262+ idents . forEach ( id => splitPropertyIdents . add ( id ) )
263+ } else {
264+ idents . forEach ( id => nonSplitPropertyIdents . add ( id ) )
265+ }
266+ } ,
267+ } )
268+
269+ // Find shared identifiers that need to be exported
270+ splitPropertyIdents . forEach ( ident => {
271+ if ( nonSplitPropertyIdents . has ( ident ) ) {
272+ sharedModuleLevelIdents . add ( ident )
273+ }
274+ } )
275+
194276 routeOptions . properties . forEach ( ( prop ) => {
195277 if ( t . isObjectProperty ( prop ) ) {
196278 if ( t . isIdentifier ( prop . key ) ) {
@@ -423,6 +505,41 @@ export function compileCodeSplitReferenceRoute(
423505 } ,
424506 } )
425507 }
508+
509+ /**
510+ * Export shared module-level variables that are used by both
511+ * split properties (e.g., component) and non-split properties (e.g., loader)
512+ * This prevents double initialization when the split file is loaded
513+ */
514+ if ( sharedModuleLevelIdents . size > 0 ) {
515+ modified = true
516+ sharedModuleLevelIdents . forEach ( ( identName ) => {
517+ const binding = programPath . scope . getBinding ( identName )
518+ if ( ! binding ) return
519+
520+ const bindingPath = binding . path
521+
522+ // Check if it's a variable declaration at the top level
523+ if (
524+ bindingPath . isVariableDeclarator ( ) &&
525+ bindingPath . parentPath ?. isVariableDeclaration ( ) &&
526+ bindingPath . parentPath . parentPath ?. isProgram ( )
527+ ) {
528+ const varDecl = bindingPath . parentPath
529+
530+ // Only export const/let declarations (not imports or functions)
531+ if (
532+ varDecl . node . kind === 'const' ||
533+ varDecl . node . kind === 'let'
534+ ) {
535+ // Convert to export declaration
536+ const exportDecl = t . exportNamedDeclaration ( varDecl . node , [ ] )
537+ varDecl . replaceWith ( exportDecl )
538+ knownExportedIdents . add ( identName )
539+ }
540+ }
541+ } )
542+ }
426543 } ,
427544 } ,
428545 } )
@@ -762,6 +879,107 @@ export function compileCodeSplitVirtualRoute(
762879 }
763880 } ,
764881 } )
882+
883+ // Convert non-exported module-level variables that are shared with the reference file
884+ // to imports. These are variables used by both split and non-split parts.
885+ // They will be exported in the reference file, so we need to import them here.
886+ const importedSharedIdents = new Set < string > ( )
887+ const sharedVariablesToImport : Array < string > = [ ]
888+
889+ programPath . traverse ( {
890+ VariableDeclaration ( varDeclPath ) {
891+ // Only process top-level const/let declarations
892+ if ( ! varDeclPath . parentPath . isProgram ( ) ) return
893+ if ( varDeclPath . node . kind !== 'const' && varDeclPath . node . kind !== 'let' ) return
894+
895+ varDeclPath . node . declarations . forEach ( ( declarator ) => {
896+ if ( ! t . isIdentifier ( declarator . id ) ) return
897+
898+ const varName = declarator . id . name
899+
900+ // Skip if the init is a function/arrow function - those are unlikely to be shared objects
901+ // We only want to import object literals and similar data structures
902+ if (
903+ t . isArrowFunctionExpression ( declarator . init ) ||
904+ t . isFunctionExpression ( declarator . init )
905+ ) {
906+ return
907+ }
908+
909+ // Check if this variable is used by the split node
910+ // If it is, it might be shared with non-split parts and should be imported
911+ const isUsedBySplitNode = intendedSplitNodes . values ( ) . next ( ) . value &&
912+ Object . values ( trackedNodesToSplitByType ) . some ( tracked => {
913+ if ( ! tracked ?. node ) return false
914+ let isUsed = false
915+ babel . traverse ( tracked . node , {
916+ Identifier ( idPath ) {
917+ if ( idPath . isReferencedIdentifier ( ) && idPath . node . name === varName ) {
918+ isUsed = true
919+ }
920+ }
921+ } , programPath . scope )
922+ return isUsed
923+ } )
924+
925+ if ( isUsedBySplitNode ) {
926+ sharedVariablesToImport . push ( varName )
927+ }
928+ } )
929+ } ,
930+ } )
931+
932+ // Remove shared variable declarations and add imports
933+ if ( sharedVariablesToImport . length > 0 ) {
934+ programPath . traverse ( {
935+ VariableDeclaration ( varDeclPath ) {
936+ if ( ! varDeclPath . parentPath . isProgram ( ) ) return
937+
938+ const declaratorsToKeep = varDeclPath . node . declarations . filter ( ( declarator ) => {
939+ if ( ! t . isIdentifier ( declarator . id ) ) return true
940+ const varName = declarator . id . name
941+
942+ if ( sharedVariablesToImport . includes ( varName ) ) {
943+ importedSharedIdents . add ( varName )
944+ return false // Remove this declarator
945+ }
946+ return true
947+ } )
948+
949+ if ( declaratorsToKeep . length === 0 ) {
950+ // Remove the entire variable declaration
951+ varDeclPath . remove ( )
952+ } else if ( declaratorsToKeep . length < varDeclPath . node . declarations . length ) {
953+ // Update with remaining declarators
954+ varDeclPath . node . declarations = declaratorsToKeep
955+ }
956+ } ,
957+ } )
958+
959+ // Add import statement for shared variables
960+ if ( importedSharedIdents . size > 0 ) {
961+ const importDecl = t . importDeclaration (
962+ Array . from ( importedSharedIdents ) . map ( ( name ) =>
963+ t . importSpecifier ( t . identifier ( name ) , t . identifier ( name ) ) ,
964+ ) ,
965+ t . stringLiteral ( removeSplitSearchParamFromFilename ( opts . filename ) ) ,
966+ )
967+ programPath . unshiftContainer ( 'body' , importDecl )
968+
969+ // Track imported identifiers for dead code elimination
970+ const importPath = programPath . get ( 'body' ) [ 0 ] as babel . NodePath
971+ importPath . traverse ( {
972+ Identifier ( identPath ) {
973+ if (
974+ identPath . parentPath . isImportSpecifier ( ) &&
975+ identPath . key === 'local'
976+ ) {
977+ refIdents . add ( identPath )
978+ }
979+ } ,
980+ } )
981+ }
982+ }
765983 } ,
766984 } ,
767985 } )
0 commit comments