@@ -129,8 +129,46 @@ export default function useCacheInline(profiles, providers = {}, type) {
129129 ...resolvedProviders ,
130130 } ;
131131
132+ // Extract all binding names from a pattern node (Identifier, ObjectPattern,
133+ // ArrayPattern, AssignmentPattern, RestElement).
134+ function collectBindingNames ( pattern , out = [ ] ) {
135+ if ( ! pattern ) return out ;
136+ switch ( pattern . type ) {
137+ case "Identifier" :
138+ out . push ( pattern . name ) ;
139+ break ;
140+ case "ObjectPattern" :
141+ for ( const prop of pattern . properties ) {
142+ if ( prop . type === "RestElement" ) {
143+ collectBindingNames ( prop . argument , out ) ;
144+ } else {
145+ collectBindingNames ( prop . value , out ) ;
146+ }
147+ }
148+ break ;
149+ case "ArrayPattern" :
150+ for ( const el of pattern . elements ) {
151+ if ( el ) collectBindingNames ( el , out ) ;
152+ }
153+ break ;
154+ case "AssignmentPattern" :
155+ collectBindingNames ( pattern . left , out ) ;
156+ break ;
157+ case "RestElement" :
158+ collectBindingNames ( pattern . argument , out ) ;
159+ break ;
160+ }
161+ return out ;
162+ }
163+
132164 const caches = [ ] ;
133- const locals = [ ] ;
165+ // Scope stack: each entry is { node, names: Set<string> } representing
166+ // a function scope. Only non-cached function scopes are pushed.
167+ // When checking closure captures for a cached function, we look at all
168+ // entries on the stack — they represent the enclosing lexical scopes.
169+ // This prevents variables from unrelated sibling functions from leaking
170+ // into the closure capture list.
171+ const scopeStack = [ ] ;
134172 let parent = null ;
135173 let useCacheNode = null ;
136174 let useCache = null ;
@@ -208,14 +246,53 @@ export default function useCacheInline(profiles, providers = {}, type) {
208246 }
209247
210248 if ( useCacheNode && node . type === "Identifier" ) {
249+ // Skip identifiers in non-reference positions (object property
250+ // keys, non-computed member expression properties, declaration
251+ // ids). After JSX transformation, JSX attribute names become
252+ // Property keys (e.g., { name: "World" }) and must not be
253+ // treated as closure variable references.
254+ const isNonRef =
255+ ( node . parent ?. type === "Property" &&
256+ node . parent . key === node &&
257+ ! node . parent . computed ) ||
258+ ( node . parent ?. type === "MemberExpression" &&
259+ node . parent . property === node &&
260+ ! node . parent . computed ) ||
261+ ( node . parent ?. type === "VariableDeclarator" &&
262+ node . parent . id === node ) ||
263+ ( ( node . parent ?. type === "FunctionDeclaration" ||
264+ node . parent ?. type === "FunctionExpression" ) &&
265+ node . parent . id === node ) ;
211266 if (
212- locals . includes ( node . name ) &&
213- ! useCache . params . includes ( node . name )
267+ ! isNonRef &&
268+ scopeStack . some ( ( s ) => s . names . has ( node . name ) ) &&
269+ ! useCache . params . includes ( node . name ) &&
270+ ! useCache . locals . includes ( node . name )
214271 ) {
215272 useCache . params . push ( node . name ) ;
216273 }
217274 }
218275
276+ // Track function/arrow parameters as locals (these are bindings in
277+ // the enclosing scope that a nested "use cache" function may close
278+ // over). Push a new scope entry onto the stack so that variables
279+ // from sibling/unrelated functions don't leak into the closure
280+ // capture list.
281+ if (
282+ ! useCacheNode &&
283+ ( node . type === "FunctionDeclaration" ||
284+ node . type === "FunctionExpression" ||
285+ node . type === "ArrowFunctionExpression" )
286+ ) {
287+ const names = new Set ( ) ;
288+ for ( const param of node . params ) {
289+ for ( const n of collectBindingNames ( param ) ) {
290+ names . add ( n ) ;
291+ }
292+ }
293+ scopeStack . push ( { node, names } ) ;
294+ }
295+
219296 if ( node . type === "VariableDeclarator" ) {
220297 let parent = node . parent ;
221298 while ( parent ) {
@@ -228,17 +305,28 @@ export default function useCacheInline(profiles, providers = {}, type) {
228305 parent = parent . parent ;
229306 }
230307 if ( parent ) {
308+ const names = collectBindingNames ( node . id ) ;
231309 if ( useCacheNode ) {
232- useCache . locals . push ( node . id . name ) ;
233- } else {
234- locals . push ( node . id . name ) ;
310+ useCache . locals . push ( ...names ) ;
311+ } else if ( scopeStack . length > 0 ) {
312+ const topScope = scopeStack [ scopeStack . length - 1 ] . names ;
313+ for ( const name of names ) topScope . add ( name ) ;
235314 }
236315 }
237316 }
238317
239318 parent = node ;
240319 } ,
241320 leave ( node ) {
321+ // Pop scope when leaving a non-cached function whose scope was
322+ // pushed in `enter`.
323+ if (
324+ scopeStack . length > 0 &&
325+ scopeStack [ scopeStack . length - 1 ] . node === node
326+ ) {
327+ scopeStack . pop ( ) ;
328+ }
329+
242330 if ( node === useCacheNode ) {
243331 if ( useCache . params . length > 0 ) {
244332 useCacheNode . type = "CallExpression" ;
@@ -264,14 +352,14 @@ export default function useCacheInline(profiles, providers = {}, type) {
264352 } ) ) ,
265353 ] ;
266354 } else if ( useCache . parent ?. type === "ExportNamedDeclaration" ) {
267- useCache . name = useCache . parent . declaration . id . name ;
355+ useCache . exported = true ;
268356 useCache . parent . parent . body =
269357 useCache . parent . parent . body . filter (
270358 ( n ) => n !== useCache . parent
271359 ) ;
272360 } else if ( useCache . parent ?. type === "ExportDefaultDeclaration" ) {
361+ useCache . exported = true ;
273362 useCache . identifier = "_default" ;
274- useCache . name = "_default" ;
275363 useCache . parent . parent . body =
276364 useCache . parent . parent . body . filter (
277365 ( n ) => n !== useCache . parent
@@ -548,11 +636,8 @@ export default function useCacheInline(profiles, providers = {}, type) {
548636 type : "ArrayPattern" ,
549637 elements : [
550638 ...cache . params . map ( ( param ) => ( {
551- type : "VariableDeclarator" ,
552- id : {
553- type : "Identifier" ,
554- name : param ,
555- } ,
639+ type : "Identifier" ,
640+ name : param ,
556641 } ) ) ,
557642 ...cache . node . params ,
558643 ] ,
@@ -790,29 +875,72 @@ export default function useCacheInline(profiles, providers = {}, type) {
790875 } ,
791876 } ,
792877 ] ;
793- ast . body . push (
794- {
795- type : "FunctionDeclaration" ,
796- async : ! ( cache . provider === "request" && isClient ) ,
797- id : {
798- type : "Identifier" ,
799- name : cache . name ,
800- } ,
801- params : [
802- ...( argsName
803- ? [
878+ ast . body . push ( {
879+ type : "FunctionDeclaration" ,
880+ async : ! ( cache . provider === "request" && isClient ) ,
881+ id : {
882+ type : "Identifier" ,
883+ name : cache . name ,
884+ } ,
885+ params : [
886+ ...( argsName
887+ ? [
888+ {
889+ type : "RestElement" ,
890+ argument : {
891+ type : "Identifier" ,
892+ name : argsName ,
893+ } ,
894+ } ,
895+ ]
896+ : cache . node . params ) ,
897+ ] ,
898+ body : cache . node . body ,
899+ } ) ;
900+
901+ // For exported cached functions, wrap in __react_cache__() so they
902+ // get the same per-request memoization as non-exported cached
903+ // functions.
904+ if ( cache . exported && cache . identifier ) {
905+ ast . body . push ( {
906+ type : "VariableDeclaration" ,
907+ kind : "const" ,
908+ declarations : [
909+ {
910+ type : "VariableDeclarator" ,
911+ id : {
912+ type : "Identifier" ,
913+ name : cache . identifier ,
914+ } ,
915+ init : {
916+ type : "CallExpression" ,
917+ callee : {
918+ type : "Identifier" ,
919+ name : "__react_cache__" ,
920+ } ,
921+ arguments : [
804922 {
805- type : "RestElement" ,
806- argument : {
807- type : "Identifier" ,
808- name : argsName ,
809- } ,
923+ type : "Identifier" ,
924+ name : cache . name ,
810925 } ,
811- ]
812- : cache . node . params ) ,
926+ ] ,
927+ } ,
928+ } ,
813929 ] ,
814- body : cache . node . body ,
815- } ,
930+ } ) ;
931+ }
932+
933+ // For nested cached functions (parent is a BlockStatement inside
934+ // another function), the identifier is scoped to the enclosing
935+ // function and not accessible at module level. Use the mangled impl
936+ // name (which is always module-level) for the CACHE_KEY/PROVIDER
937+ // assignments.
938+ const isModuleScoped =
939+ cache . exported || cache . parent ?. type === "Program" ;
940+ const cacheTarget = isModuleScoped
941+ ? ( cache . identifier ?? cache . name )
942+ : cache . name ;
943+ ast . body . push (
816944 {
817945 type : "ExpressionStatement" ,
818946 expression : {
@@ -823,7 +951,7 @@ export default function useCacheInline(profiles, providers = {}, type) {
823951 computed : true ,
824952 object : {
825953 type : "Identifier" ,
826- name : cache . identifier ?? cache . name ,
954+ name : cacheTarget ,
827955 } ,
828956 property : {
829957 type : "Identifier" ,
@@ -843,7 +971,7 @@ export default function useCacheInline(profiles, providers = {}, type) {
843971 computed : true ,
844972 object : {
845973 type : "Identifier" ,
846- name : cache . identifier ?? cache . name ,
974+ name : cacheTarget ,
847975 } ,
848976 property : {
849977 type : "Identifier" ,
@@ -863,14 +991,38 @@ export default function useCacheInline(profiles, providers = {}, type) {
863991 ast . body . push ( {
864992 type : "ExportNamedDeclaration" ,
865993 declaration : null ,
866- specifiers : caches . map ( ( cache ) => ( {
867- type : "ExportSpecifier" ,
868- exported : {
869- type : "Identifier" ,
870- name : cache . name === "_default" ? "default" : cache . name ,
871- } ,
872- local : { type : "Identifier" , name : cache . name } ,
873- } ) ) ,
994+ specifiers : caches . flatMap ( ( cache ) => {
995+ const specs = [
996+ {
997+ type : "ExportSpecifier" ,
998+ exported : {
999+ type : "Identifier" ,
1000+ name : cache . name === "_default" ? "default" : cache . name ,
1001+ } ,
1002+ local : { type : "Identifier" , name : cache . name } ,
1003+ } ,
1004+ ] ;
1005+ // For exported cached functions, also export the user-facing
1006+ // identifier (which is the __react_cache__-wrapped version).
1007+ if (
1008+ cache . exported &&
1009+ cache . identifier &&
1010+ cache . identifier !== cache . name
1011+ ) {
1012+ specs . push ( {
1013+ type : "ExportSpecifier" ,
1014+ exported : {
1015+ type : "Identifier" ,
1016+ name :
1017+ cache . identifier === "_default"
1018+ ? "default"
1019+ : cache . identifier ,
1020+ } ,
1021+ local : { type : "Identifier" , name : cache . identifier } ,
1022+ } ) ;
1023+ }
1024+ return specs ;
1025+ } ) ,
8741026 } ) ;
8751027
8761028 return codegen ( ast , id ) ;
0 commit comments